From 45dc157643f1521b9eb168f0de0587acbe355c49 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 23 May 2024 12:12:09 +0200 Subject: [PATCH 01/32] Added: track WooCommerce purchases on thank you page. --- src/Compatibility.php | 2 +- src/ECommerce.php | 39 +++++++++++++++ src/ECommerce/WooCommerce.php | 91 +++++++++++++++++++++++++++++++++++ src/Plugin.php | 1 + 4 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 src/ECommerce.php create mode 100644 src/ECommerce/WooCommerce.php diff --git a/src/Compatibility.php b/src/Compatibility.php index 108b4923..65014d23 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -1,6 +1,6 @@ document.addEventListener("DOMContentLoaded", () => { %s });'; + + /** + * Build class. + */ + public function __construct() { + $this->init(); + } + + /** + * Execute Ecommerce integrations. + * + * @return void + */ + private function init() { + // WooCommerce + if ( function_exists( 'WC' ) ) { + new ECommerce\WooCommerce(); + } + + // Easy Digital Downloads + if ( function_exists( 'EDD' ) ) { + // new ECommerce\EDD(); + } + } +} diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php new file mode 100644 index 00000000..a2ab7b97 --- /dev/null +++ b/src/ECommerce/WooCommerce.php @@ -0,0 +1,91 @@ +init(); + } + + /** + * Filter and action hooks. + * + * @return void + */ + private function init() { + add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); + } + + /** + * Track WooCommerce purchase on thank you page. + * + * @param $order_id + * + * @return void + */ + public function track_purchase( $order_id ) { + $order = wc_get_order( $order_id ); + $is_tracked = $order->get_meta( self::PURCHASE_TRACKED_META_KEY ); + + if ( $is_tracked ) { + return; + } + + $items = $this->get_items( $order ); + $props = apply_filters( + 'plausible_analytics_ecommerce_woocommerce_track_purchase_custom_properties', + [ + 'transaction_id' => $order->get_transaction_id(), + 'items' => $items, + ] + ); + $props = wp_json_encode( + [ + 'revenue' => [ 'amount' => number_format_i18n( $order->get_total(), 2 ), 'currency' => $order->get_currency() ], + 'props' => $props, + ] + ); + $event_label = __( 'Purchase', 'plausible-analytics' ); + + echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); + + $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); + } + + /** + * Returns an array of order item data. + * + * @param $order + * + * @return array + */ + private function get_items( $order ) { + $order_items = $order->get_items(); + $items = []; + + foreach ( $order_items as $id => $item ) { + $items[] = $item->get_data(); + } + + foreach ( $items as &$item ) { + unset( $item[ 'taxes' ] ); + unset( $item[ 'meta_data' ] ); + } + + return $items; + } +} diff --git a/src/Plugin.php b/src/Plugin.php index 6518dc3c..cc59b4d4 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -43,6 +43,7 @@ public function register_services() { new Actions(); new Ajax(); new Compatibility(); + new ECommerce(); new Filters(); new Proxy(); new Setup(); From 40dd7357030e7db87f255eae6d4ea2f0353fcafc Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 23 May 2024 12:13:04 +0200 Subject: [PATCH 02/32] Fixed: make sure purchases aren't tracked twice. --- src/ECommerce/WooCommerce.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php index a2ab7b97..1317be84 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/ECommerce/WooCommerce.php @@ -64,6 +64,7 @@ public function track_purchase( $order_id ) { echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); + $order->save(); } /** From 4839f36ff3ac16f93fc80bdb6929284953e41e38 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 23 May 2024 15:19:36 +0200 Subject: [PATCH 03/32] Fixed: track Add to cart and Remove cart item events. --- src/Admin/Module.php | 29 ++------- src/ECommerce/WooCommerce.php | 109 +++++++++++++++++++++++++++++++++- src/Proxy.php | 78 +++++++++++++++++------- 3 files changed, 168 insertions(+), 48 deletions(-) diff --git a/src/Admin/Module.php b/src/Admin/Module.php index 11168361..5b499b56 100644 --- a/src/Admin/Module.php +++ b/src/Admin/Module.php @@ -11,6 +11,7 @@ use Exception; use Plausible\Analytics\WP\Helpers; +use Plausible\Analytics\WP\Proxy; class Module { /** @@ -252,31 +253,9 @@ private function test_proxy( $run = true ) { return false; // @codeCoverageIgnore } - $namespace = Helpers::get_proxy_resource( 'namespace' ); - $base = Helpers::get_proxy_resource( 'base' ); - $endpoint = Helpers::get_proxy_resource( 'endpoint' ); - $request = new \WP_REST_Request( 'POST', "/$namespace/v1/$base/$endpoint" ); - $request->set_body( - wp_json_encode( - [ - 'd' => 'plausible.test', - 'n' => 'pageview', - 'u' => 'https://plausible.test/test', - ] - ) - ); - - /** @var \WP_REST_Response $result */ - try { - $result = rest_do_request( $request ); - } catch ( \Exception $e ) { // @codeCoverageIgnore - /** - * There's no need to handle the error, because we don't want to display it anyway. - * We'll leave the parameter for backwards compatibility. - */ - return false; // @codeCoverageIgnore - } + $proxy = new Proxy( false ); + $result = $proxy->do_request( 'plausible.test', 'pageview', 'https://plausible.test/test' ); - return wp_remote_retrieve_response_code( $result->get_data() ) === 202; + return wp_remote_retrieve_response_code( $result ) === 202; } } diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php index 1317be84..5cc6f610 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/ECommerce/WooCommerce.php @@ -10,10 +10,27 @@ namespace Plausible\Analytics\WP\ECommerce; use Plausible\Analytics\WP\ECommerce; +use Plausible\Analytics\WP\Proxy; +use WC_Cart; class WooCommerce { const PURCHASE_TRACKED_META_KEY = '_plausible_analytics_purchase_tracked'; + const CUSTOM_PROPERTIES = [ + 'id', + 'order_id', + 'name', + 'price', + 'product_id', + 'variation_id', + 'quantity', + 'tax_class', + 'subtotal', + 'subtotal_tax', + 'total', + 'total_tax', + ]; + /** * Build class. */ @@ -28,6 +45,8 @@ public function __construct() { */ private function init() { add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); + add_action( 'woocommerce_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 4 ); + add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); } /** @@ -83,10 +102,96 @@ private function get_items( $order ) { } foreach ( $items as &$item ) { - unset( $item[ 'taxes' ] ); - unset( $item[ 'meta_data' ] ); + $item = $this->clean_data( $item ); } return $items; } + + /** + * Removes unneeded elements from the array. + * + * @param array $product Product Data. + * + * @return mixed + */ + private function clean_data( $product ) { + foreach ( $product as $key => $value ) { + if ( ! in_array( $key, self::CUSTOM_PROPERTIES ) ) { + unset( $product[ $key ] ); + } + } + + return $product; + } + + /** + * @param string $item_cart_id ID of the item in the cart. + * @param string $product_id ID of the product added to the cart. + * @param $quantity + * @param $variation_id + * + * @return void + */ + public function track_add_to_cart( $item_cart_id, $product_id, $quantity, $variation_id ) { + $product = wc_get_product( $product_id ); + $product_data = $this->clean_data( $product->get_data() ); + $cart = wc()->cart; + $cart_contents = $cart->get_cart_contents(); + + foreach ( $cart_contents as &$cart_item ) { + $cart_item = $this->clean_data( $cart_item ); + } + + $added_to_cart = [ + 'quantity' => $quantity, + 'variation_id' => $variation_id, + ]; + $props = apply_filters( + 'plausible_analytics_ecommerce_woocommerce_track_add_to_cart_custom_properties', + [ + 'props' => [ + 'item' => array_merge( $added_to_cart, $product_data ), + 'cart' => $cart_contents, + ], + ] + ); + $event_label = __( 'Add to cart', 'plausible-analytics' ); + $proxy = new Proxy( false ); + + $proxy->do_request( wp_get_referer(), $event_label, $props ); + } + + /** + * Track Remove from cart events. + * + * @param string $cart_item_key Key of item being removed from cart. + * @param WC_Cart $cart Instance of the current cart. + * + * @return void + */ + public function track_remove_cart_item( $cart_item_key, $cart ) { + $cart_contents = $cart->get_cart_contents(); + $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] ); + + unset( $cart_contents[ $cart_item_key ] ); + + foreach ( $cart_contents as &$item_in_cart ) { + $item_in_cart = $this->clean_data( $item_in_cart ); + } + + $props = apply_filters( + 'plausible_analytics_ecommerce_woocommerce_track_remove_cart_item_custom_properties', + [ + 'props' => [ + 'removed_item' => $item_removed_from_cart, + 'cart' => $cart_contents, + ], + ] + ); + $event_label = __( 'Remove cart item', 'plausible-analytics' ); + $proxy = new Proxy( false ); + + $proxy->do_request( wp_get_referer(), $event_label, $props ); + } } diff --git a/src/Proxy.php b/src/Proxy.php index b140ac07..5ac05929 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -13,6 +13,7 @@ namespace Plausible\Analytics\WP; use Exception; +use WP_Error; class Proxy { /** @@ -56,12 +57,12 @@ class Proxy { * @return void * @throws Exception */ - public function __construct() { + public function __construct( $init = true ) { $this->namespace = Helpers::get_proxy_resource( 'namespace' ) . '/v1'; $this->base = Helpers::get_proxy_resource( 'base' ); $this->endpoint = Helpers::get_proxy_resource( 'endpoint' ); - $this->init(); + $this->init( $init ); } /** @@ -69,7 +70,11 @@ public function __construct() { * * @return void */ - private function init() { + private function init( $init ) { + if ( ! $init ) { + return; + } + $settings = []; if ( array_key_exists( 'option_name', $_POST ) && @@ -86,30 +91,40 @@ private function init() { } /** - * Register the API route. + * A public wrapper to programmatically send hits to the Plausible API. * - * @return void + * @see https://plausible.io/docs/events-api + * + * @param string $url URL of the page where the event was triggered. + * @param string $name Name of the event, defaults to 'pageview', all other names are treated as custom events by the API. + * @param array $props Custom properties for the event. + * @param string $domain Domain of the site in Plausible where the event should be registered. + * + * @return array|WP_Error */ - public function register_route() { - register_rest_route( - $this->namespace, - '/' . $this->base . '/' . $this->endpoint, - [ - [ - 'methods' => 'POST', - 'callback' => [ $this, 'send' ], - // There's no reason not to allow access to this API. - 'permission_callback' => '__return_true', - ], - 'schema' => null, - ] - ); + public function do_request( $url, $name = 'pageview', $props = [], $domain = '' ) { + $request = new \WP_REST_Request( 'POST', "/$this->namespace/v1/$this->base/$this->endpoint" ); + $body = [ + 'u' => $url, + 'n' => $name, + 'd' => $domain ?: Helpers::get_domain(), + ]; + + if ( ! empty( $props ) ) { + $body[ 'p' ] = $props; + } + + $request->set_body( wp_json_encode( $body ) ); + + return $this->send_event( $request ); } /** - * @return array|\WP_Error + * Formats and sends $request to the Plausible API. + * + * @return array|WP_Error */ - public function send( $request ) { + public function send_event( $request ) { $params = $request->get_body(); $ip = $this->get_user_ip_address(); @@ -164,4 +179,25 @@ private function get_user_ip_address() { private function header_exists( $global ) { return ! empty( $_SERVER[ $global ] ); } + + /** + * Register the API route. + * + * @return void + */ + public function register_route() { + register_rest_route( + $this->namespace, + '/' . $this->base . '/' . $this->endpoint, + [ + [ + 'methods' => 'POST', + 'callback' => [ $this, 'send_event' ], + // There's no reason not to allow access to this API. + 'permission_callback' => '__return_true', + ], + 'schema' => null, + ] + ); + } } From e78ff715afee6047839e8ecd272470f69cb37479 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 24 May 2024 10:15:03 +0200 Subject: [PATCH 04/32] Improved: assure our HTTP response code is returned when using the Proxy. --- src/Proxy.php | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Proxy.php b/src/Proxy.php index 5ac05929..119416a7 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -14,6 +14,9 @@ use Exception; use WP_Error; +use WP_HTTP_Response; +use WP_REST_Request; +use WP_REST_Server; class Proxy { /** @@ -88,6 +91,8 @@ private function init( $init ) { if ( Helpers::proxy_enabled( $settings ) ) { add_action( 'rest_api_init', [ $this, 'register_route' ] ); } + + add_filter( 'rest_post_dispatch', [ $this, 'force_http_response_code' ], null, 3 ); } /** @@ -200,4 +205,24 @@ public function register_route() { ] ); } + + /** + * Make sure our response code is returned, instead of the default 200 on success. + * + * @param WP_HTTP_Response $response + * @param WP_REST_Server $server + * @param WP_REST_Request $request + * + * @return WP_HTTP_Response + */ + public function force_http_response_code( $response, $server, $request ) { + if ( strpos( $request->get_route(), $this->namespace ) === false ) { + return $response; + } + + $response_code = wp_remote_retrieve_response_code( $response->get_data() ); + $response->set_status( $response_code ); + + return $response; + } } From 23ff68058b24d84622675245d0fce15dc2f7185a Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 24 May 2024 10:23:27 +0200 Subject: [PATCH 05/32] Ignore safety check. --- src/Proxy.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Proxy.php b/src/Proxy.php index 119416a7..61e09b9c 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -217,7 +217,7 @@ public function register_route() { */ public function force_http_response_code( $response, $server, $request ) { if ( strpos( $request->get_route(), $this->namespace ) === false ) { - return $response; + return $response; // @codeCoverageIgnore } $response_code = wp_remote_retrieve_response_code( $response->get_data() ); From d17d98291cd75cbddebb5502f6aa390f9142995f Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 24 May 2024 10:39:30 +0200 Subject: [PATCH 06/32] Added: track entered checkout event. --- src/ECommerce/WooCommerce.php | 186 +++++++++++++++++++--------------- 1 file changed, 107 insertions(+), 79 deletions(-) diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php index 5cc6f610..f0af21ce 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/ECommerce/WooCommerce.php @@ -44,85 +44,10 @@ public function __construct() { * @return void */ private function init() { - add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); add_action( 'woocommerce_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 4 ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); - } - - /** - * Track WooCommerce purchase on thank you page. - * - * @param $order_id - * - * @return void - */ - public function track_purchase( $order_id ) { - $order = wc_get_order( $order_id ); - $is_tracked = $order->get_meta( self::PURCHASE_TRACKED_META_KEY ); - - if ( $is_tracked ) { - return; - } - - $items = $this->get_items( $order ); - $props = apply_filters( - 'plausible_analytics_ecommerce_woocommerce_track_purchase_custom_properties', - [ - 'transaction_id' => $order->get_transaction_id(), - 'items' => $items, - ] - ); - $props = wp_json_encode( - [ - 'revenue' => [ 'amount' => number_format_i18n( $order->get_total(), 2 ), 'currency' => $order->get_currency() ], - 'props' => $props, - ] - ); - $event_label = __( 'Purchase', 'plausible-analytics' ); - - echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); - - $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); - $order->save(); - } - - /** - * Returns an array of order item data. - * - * @param $order - * - * @return array - */ - private function get_items( $order ) { - $order_items = $order->get_items(); - $items = []; - - foreach ( $order_items as $id => $item ) { - $items[] = $item->get_data(); - } - - foreach ( $items as &$item ) { - $item = $this->clean_data( $item ); - } - - return $items; - } - - /** - * Removes unneeded elements from the array. - * - * @param array $product Product Data. - * - * @return mixed - */ - private function clean_data( $product ) { - foreach ( $product as $key => $value ) { - if ( ! in_array( $key, self::CUSTOM_PROPERTIES ) ) { - unset( $product[ $key ] ); - } - } - - return $product; + add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); + add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); } /** @@ -156,12 +81,29 @@ public function track_add_to_cart( $item_cart_id, $product_id, $quantity, $varia ], ] ); - $event_label = __( 'Add to cart', 'plausible-analytics' ); + $event_label = __( 'Add Item To Cart', 'plausible-analytics' ); $proxy = new Proxy( false ); $proxy->do_request( wp_get_referer(), $event_label, $props ); } + /** + * Removes unneeded elements from the array. + * + * @param array $product Product Data. + * + * @return mixed + */ + private function clean_data( $product ) { + foreach ( $product as $key => $value ) { + if ( ! in_array( $key, self::CUSTOM_PROPERTIES ) ) { + unset( $product[ $key ] ); + } + } + + return $product; + } + /** * Track Remove from cart events. * @@ -189,9 +131,95 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { ], ] ); - $event_label = __( 'Remove cart item', 'plausible-analytics' ); + $event_label = __( 'Remove Cart Item', 'plausible-analytics' ); $proxy = new Proxy( false ); $proxy->do_request( wp_get_referer(), $event_label, $props ); } + + /** + * @return void + */ + public function track_entered_checkout() { + if ( ! is_checkout() ) { + return; + } + + $cart = WC()->cart; + $cart_contents = $cart->get_cart_contents(); + + foreach ( $cart_contents as &$cart_item ) { + $cart_item = $this->clean_data( $cart_item ); + } + + $props = apply_filters( + 'plausible_analytics_ecommerce_woocommerce_track_entered_checkout_custom_properties', + [ + 'cart' => $cart_contents, + ] + ); + $props = wp_json_encode( $props ); + $event_label = __( 'Entered Checkout', 'plausible-analytics' ); + + echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); + } + + /** + * Track WooCommerce purchase on thank you page. + * + * @param $order_id + * + * @return void + */ + public function track_purchase( $order_id ) { + $order = wc_get_order( $order_id ); + $is_tracked = $order->get_meta( self::PURCHASE_TRACKED_META_KEY ); + + if ( $is_tracked ) { + return; + } + + $items = $this->get_items( $order ); + $props = apply_filters( + 'plausible_analytics_ecommerce_woocommerce_track_purchase_custom_properties', + [ + 'transaction_id' => $order->get_transaction_id(), + 'items' => $items, + ] + ); + $props = wp_json_encode( + [ + 'revenue' => [ 'amount' => number_format_i18n( $order->get_total(), 2 ), 'currency' => $order->get_currency() ], + 'props' => $props, + ] + ); + $event_label = __( 'Purchase', 'plausible-analytics' ); + + echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); + + $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); + $order->save(); + } + + /** + * Returns an array of order item data. + * + * @param $order + * + * @return array + */ + private function get_items( $order ) { + $order_items = $order->get_items(); + $items = []; + + foreach ( $order_items as $id => $item ) { + $items[] = $item->get_data(); + } + + foreach ( $items as &$item ) { + $item = $this->clean_data( $item ); + } + + return $items; + } } From d9a61513aff34844e977aa3d424f76c82a553ba6 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Tue, 28 May 2024 20:51:53 +0200 Subject: [PATCH 07/32] Remove Babel, since we no longer need it. --- webpack.config.js | 151 ++++++++++++++++++++++------------------------ 1 file changed, 72 insertions(+), 79 deletions(-) diff --git a/webpack.config.js b/webpack.config.js index 3d7b18c3..7f12cd5c 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -1,95 +1,88 @@ -const path = require( 'path' ); -const CopyWebpackPlugin = require( 'copy-webpack-plugin' ); -const MiniCSSExtractPlugin = require( 'mini-css-extract-plugin' ); -const ImageminPlugin = require( 'imagemin-webpack-plugin' ).default; -const { CleanWebpackPlugin } = require( 'clean-webpack-plugin' ); -const wpPot = require( 'wp-pot' ); +const path = require('path'); +const CopyWebpackPlugin = require('copy-webpack-plugin'); +const MiniCSSExtractPlugin = require('mini-css-extract-plugin'); +const ImageminPlugin = require('imagemin-webpack-plugin').default; +const {CleanWebpackPlugin} = require('clean-webpack-plugin'); +const wpPot = require('wp-pot'); -const inProduction = ( 'production' === process.env.NODE_ENV ); +const inProduction = ('production' === process.env.NODE_ENV); const mode = inProduction ? 'production' : 'development'; const config = { - devtool: inProduction ? 'inline-source-map' : 'eval-cheap-module-source-map', - mode, - entry: { - 'plausible-admin': [ './assets/src/css/admin/main.css', './assets/src/js/admin/main.js' ], - }, - output: { - path: path.join( __dirname, './assets/dist/' ), - filename: 'js/[name].js', - }, - module: { - rules: [ + devtool: inProduction ? 'inline-source-map' : 'eval-cheap-module-source-map', + mode, + entry: { + 'plausible-admin': ['./assets/src/css/admin/main.css', './assets/src/js/admin/main.js'], + 'plausible-compatibility-woocommerce': ['./assets/src/js/compatibility/woocommerce.js'] + }, + output: { + path: path.join(__dirname, './assets/dist/'), + filename: 'js/[name].js', + }, + module: { + rules: [ + // Tailwind CSS. + { + test: /\.css$/, + use: [ + MiniCSSExtractPlugin.loader, + 'css-loader', + 'postcss-loader', + ], + }, - // Use Babel to compile JS. - { - test: /\.js$/, - exclude: /node_modules/, - loader: 'babel-loader', - }, + // Image files. + { + test: /\.(png|jpe?g|gif|svg)$/, + use: [ + { + loader: 'file-loader', + options: { + name: 'images/[name].[ext]', + publicPath: '../', + }, + }, + ], + }, + ], + }, - // Tailwind CSS. - { - test: /\.css$/, - use: [ - MiniCSSExtractPlugin.loader, - 'css-loader', - 'postcss-loader', - ], - }, + // Plugins. Gotta have 'em. + plugins: [ - // Image files. - { - test: /\.(png|jpe?g|gif|svg)$/, - use: [ - { - loader: 'file-loader', - options: { - name: 'images/[name].[ext]', - publicPath: '../', - }, - }, - ], - }, - ], - }, + // Removes the "dist" folder before building. + new CleanWebpackPlugin(), - // Plugins. Gotta have em'. - plugins: [ + new MiniCSSExtractPlugin({ + filename: 'css/[name].css', + }), - // Removes the "dist" folder before building. - new CleanWebpackPlugin(), + new CopyWebpackPlugin( + { + patterns: [ + {from: 'assets/src/images', to: 'images'}, + ], + } + ), - new MiniCSSExtractPlugin( { - filename: 'css/[name].css', - } ), - - new CopyWebpackPlugin( - { - patterns: [ - { from: 'assets/src/images', to: 'images' }, - ], - } - ), - - ], + ], }; -if ( inProduction ) { - // Minify images. - // Must go after CopyWebpackPlugin above: https://github.com/Klathmon/imagemin-webpack-plugin#example-usage - config.plugins.push( new ImageminPlugin( { test: /\.(jpe?g|png|gif|svg)$/i } ) ); +if (inProduction) { + // Minify images. + // Must go after CopyWebpackPlugin above: https://github.com/Klathmon/imagemin-webpack-plugin#example-usage + config.plugins.push(new ImageminPlugin({test: /\.(jpe?g|png|gif|svg)$/i})); - // POT file. - wpPot( { - package: 'Plausible Analytics', - domain: 'plausible-analytics', - destFile: 'languages/plausible-analytics.pot', - relativeTo: './', - src: [ './**/*.php', '!./includes/libraries/**/*', '!./vendor/**/*' ], - bugReport: 'https://github.com/plausible/wordpress/issues/new', - team: 'Plausible Analytics Team ', - } ); + // POT file. + wpPot({ + package: 'Plausible Analytics', + domain: 'plausible-analytics', + destFile: 'languages/plausible-analytics.pot', + relativeTo: './', + src: ['./**/*.php', '!./includes/libraries/**/*', '!./vendor/**/*'], + bugReport: 'https://github.com/plausible/wordpress/issues/new', + team: 'Plausible Analytics Team ', + }); } module.exports = config; From d16c141e4c20630fddd8357a467fa30b7a0ddeab Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Tue, 28 May 2024 20:52:19 +0200 Subject: [PATCH 08/32] Fixed: assure that the _wp_http_referer header is always available when tracking WooCommerce Add To/Remove From Cart events. --- assets/src/js/compatibility/woocommerce.js | 28 ++++++++++++++++ src/Admin/Module.php | 2 +- src/Compatibility.php | 31 ++++++++--------- src/ECommerce/WooCommerce.php | 39 ++++++++++++++++++++-- src/Proxy.php | 8 ++--- 5 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 assets/src/js/compatibility/woocommerce.js diff --git a/assets/src/js/compatibility/woocommerce.js b/assets/src/js/compatibility/woocommerce.js new file mode 100644 index 00000000..a714a18c --- /dev/null +++ b/assets/src/js/compatibility/woocommerce.js @@ -0,0 +1,28 @@ +/** + * Plausible Analytics + * + * WooCommerce compatibility JS. + */ +const {fetch: originalFetch} = window; + +window.fetch = (...args) => { + let [resource, config] = args; + + if (config.body === undefined) { + return originalFetch(resource, config); + } + + let data = JSON.parse(config.body); + + data.requests.forEach(function (request) { + if (!request.path.includes('cart/add-item')) { + return; + } + + request.body._wp_http_referer = window.location.href; + }); + + config.body = JSON.stringify(data); + + return originalFetch(resource, config); +}; diff --git a/src/Admin/Module.php b/src/Admin/Module.php index 5b499b56..5be7c0f8 100644 --- a/src/Admin/Module.php +++ b/src/Admin/Module.php @@ -254,7 +254,7 @@ private function test_proxy( $run = true ) { } $proxy = new Proxy( false ); - $result = $proxy->do_request( 'plausible.test', 'pageview', 'https://plausible.test/test' ); + $result = $proxy->do_request( 'pageview', 'plausible.test', 'https://plausible.test/test' ); return wp_remote_retrieve_response_code( $result ) === 202; } diff --git a/src/Compatibility.php b/src/Compatibility.php index 65014d23..1ace0275 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -24,12 +24,11 @@ public function __construct() { add_filter( 'autoptimize_filter_js_exclude', [ $this, 'exclude_plausible_js_as_string' ] ); } - // WP Rocket - if ( defined( 'WP_ROCKET_VERSION' ) ) { - add_filter( 'rocket_excluded_inline_js_content', [ $this, 'exclude_plausible_inline_js' ] ); - add_filter( 'rocket_exclude_js', [ $this, 'exclude_plausible_js' ] ); - add_filter( 'rocket_minify_excluded_external_js', [ $this, 'exclude_plausible_js' ] ); - add_filter( 'rocket_delay_js_scripts', [ $this, 'exclude_plausible_js' ] ); + // LiteSpeed Cache + if ( defined( 'LSCWP_V' ) ) { + add_filter( 'litespeed_optimize_js_excludes', [ $this, 'exclude_plausible_js' ] ); + add_filter( 'litespeed_optm_js_defer_exc', [ $this, 'exclude_plausible_inline_js' ] ); + add_filter( 'litespeed_optm_gm_js_exc', [ $this, 'exclude_plausible_inline_js' ] ); } // SG Optimizer @@ -41,20 +40,22 @@ public function __construct() { add_filter( 'sgo_javascript_combine_excluded_external_paths', [ $this, 'exclude_plausible_js' ] ); } + // WPML + if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { + add_filter( 'rest_url', [ $this, 'wpml_compatibility' ], 10, 1 ); + } + // WP Optimize if ( defined( 'WPO_VERSION' ) ) { add_filter( 'wp-optimize-minify-default-exclusions', [ $this, 'exclude_plausible_js' ] ); } - // LiteSpeed Cache - if ( defined( 'LSCWP_V' ) ) { - add_filter( 'litespeed_optimize_js_excludes', [ $this, 'exclude_plausible_js' ] ); - add_filter( 'litespeed_optm_js_defer_exc', [ $this, 'exclude_plausible_inline_js' ] ); - add_filter( 'litespeed_optm_gm_js_exc', [ $this, 'exclude_plausible_inline_js' ] ); - } - - if ( defined( 'ICL_SITEPRESS_VERSION' ) ) { - add_filter( 'rest_url', [ $this, 'wpml_compatibility' ], 10, 1 ); + // WP Rocket + if ( defined( 'WP_ROCKET_VERSION' ) ) { + add_filter( 'rocket_excluded_inline_js_content', [ $this, 'exclude_plausible_inline_js' ] ); + add_filter( 'rocket_exclude_js', [ $this, 'exclude_plausible_js' ] ); + add_filter( 'rocket_minify_excluded_external_js', [ $this, 'exclude_plausible_js' ] ); + add_filter( 'rocket_delay_js_scripts', [ $this, 'exclude_plausible_js' ] ); } } diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php index f0af21ce..117a8f23 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/ECommerce/WooCommerce.php @@ -44,12 +44,47 @@ public function __construct() { * @return void */ private function init() { + add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 ); + add_filter( 'woocommerce_store_api_add_to_cart_data', [ $this, 'add_http_referer' ], 10, 2 ); add_action( 'woocommerce_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 4 ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); } + /** + * Enqueue required JS in frontend. + * + * @return void + */ + public function add_js() { + wp_enqueue_script( + 'plausible-woocommerce-compatibility', + PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-compatibility-woocommerce.js', + [], + filemtime( PLAUSIBLE_ANALYTICS_PLUGIN_DIR . 'assets/dist/js/plausible-compatibility-woocommerce.js' ) + ); + } + + /** + * A bit of a hacky approach to ensuring the _wp_http_referer header is available to us when hitting the Proxy in @see self::track_add_to_cart() + * and @see self::track_remove_cart_item(). + * + * @param $add_to_cart_data + * @param $request + * + * @return mixed + */ + public function add_http_referer( $add_to_cart_data, $request ) { + $http_referer = $request->get_param( '_wp_http_referer' ); + + if ( ! empty( $http_referer ) ) { + $_REQUEST[ '_wp_http_referer' ] = sanitize_url( $http_referer ); + } + + return $add_to_cart_data; + } + /** * @param string $item_cart_id ID of the item in the cart. * @param string $product_id ID of the product added to the cart. @@ -84,7 +119,7 @@ public function track_add_to_cart( $item_cart_id, $product_id, $quantity, $varia $event_label = __( 'Add Item To Cart', 'plausible-analytics' ); $proxy = new Proxy( false ); - $proxy->do_request( wp_get_referer(), $event_label, $props ); + $proxy->do_request( $event_label, null, null, $props ); } /** @@ -134,7 +169,7 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { $event_label = __( 'Remove Cart Item', 'plausible-analytics' ); $proxy = new Proxy( false ); - $proxy->do_request( wp_get_referer(), $event_label, $props ); + $proxy->do_request( $event_label, null, null, $props ); } /** diff --git a/src/Proxy.php b/src/Proxy.php index 61e09b9c..2a3a8e1a 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -100,19 +100,19 @@ private function init( $init ) { * * @see https://plausible.io/docs/events-api * - * @param string $url URL of the page where the event was triggered. * @param string $name Name of the event, defaults to 'pageview', all other names are treated as custom events by the API. - * @param array $props Custom properties for the event. * @param string $domain Domain of the site in Plausible where the event should be registered. + * @param string $url URL of the page where the event was triggered. + * @param array $props Custom properties for the event. * * @return array|WP_Error */ - public function do_request( $url, $name = 'pageview', $props = [], $domain = '' ) { + public function do_request( $name = 'pageview', $domain = '', $url = '', $props = [] ) { $request = new \WP_REST_Request( 'POST', "/$this->namespace/v1/$this->base/$this->endpoint" ); $body = [ - 'u' => $url, 'n' => $name, 'd' => $domain ?: Helpers::get_domain(), + 'u' => $url ?: wp_get_referer(), ]; if ( ! empty( $props ) ) { From 9a85fad9e061eabb1f7721c07f048341765868ff Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Wed, 29 May 2024 17:14:02 +0200 Subject: [PATCH 09/32] Fixed: re-factored Custom Properties to be sent, so they're properly displayed in Plausible. --- src/ECommerce/WooCommerce.php | 178 +++++++++++++++++++--------------- 1 file changed, 100 insertions(+), 78 deletions(-) diff --git a/src/ECommerce/WooCommerce.php b/src/ECommerce/WooCommerce.php index 117a8f23..2c95715a 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/ECommerce/WooCommerce.php @@ -12,29 +12,59 @@ use Plausible\Analytics\WP\ECommerce; use Plausible\Analytics\WP\Proxy; use WC_Cart; +use WC_Product; class WooCommerce { const PURCHASE_TRACKED_META_KEY = '_plausible_analytics_purchase_tracked'; const CUSTOM_PROPERTIES = [ + 'cart_total', + 'cart_total_items', + 'customer_id', 'id', - 'order_id', 'name', + 'order_id', 'price', 'product_id', - 'variation_id', 'quantity', - 'tax_class', + 'shipping', 'subtotal', 'subtotal_tax', + 'tax_class', 'total', 'total_tax', + 'variation_id', ]; + /** + * @var string $track_add_to_cart_event_label + */ + private $track_add_to_cart_event_label; + + /** + * @var string $track_remove_cart_item_event_label + */ + private $track_remove_cart_item_event_label; + + /** + * @var string $track_entered_checkout_event_label + */ + private $track_entered_checkout_event_label; + + /** + * @var string $track_purchase_event_label + */ + private $track_purchase_event_label; + /** * Build class. */ public function __construct() { + $this->track_add_to_cart_event_label = __( 'Add Item To Cart', 'plausible-analytics' ); + $this->track_remove_cart_item_event_label = __( 'Remove Cart Item', 'plausible-analytics' ); + $this->track_entered_checkout_event_label = __( 'Entered Checkout', 'plausible-analytics' ); + $this->track_purchase_event_label = __( 'Purchase', 'plausible-analytics' ); + $this->init(); } @@ -46,7 +76,12 @@ public function __construct() { private function init() { add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 ); add_filter( 'woocommerce_store_api_add_to_cart_data', [ $this, 'add_http_referer' ], 10, 2 ); - add_action( 'woocommerce_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 4 ); + add_filter( 'woocommerce_before_add_to_cart_button', [ $this, 'insert_track_form_submit_class_name' ] ); + /** + * @todo We should use woocommerce_add_to_cart action instead, but that currently doesn't trigger on consecutive adds to the cart. Fix when resolved in WC. + * @see https://wordpress.org/support/topic/woocommerce_add_to_cart-action-isnt-triggered-on-consecutive-items/ + */ + add_filter( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); @@ -58,6 +93,11 @@ private function init() { * @return void */ public function add_js() { + // Causes errors in checkout and isn't needed either way. + if ( is_checkout() ) { + return; + } + wp_enqueue_script( 'plausible-woocommerce-compatibility', PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-compatibility-woocommerce.js', @@ -86,40 +126,51 @@ public function add_http_referer( $add_to_cart_data, $request ) { } /** - * @param string $item_cart_id ID of the item in the cart. - * @param string $product_id ID of the product added to the cart. - * @param $quantity - * @param $variation_id + * A hacky approach (with lack of a proper solution) to make sure Add To Cart events are tracked on simple product pages. * * @return void */ - public function track_add_to_cart( $item_cart_id, $product_id, $quantity, $variation_id ) { - $product = wc_get_product( $product_id ); - $product_data = $this->clean_data( $product->get_data() ); - $cart = wc()->cart; - $cart_contents = $cart->get_cart_contents(); - - foreach ( $cart_contents as &$cart_item ) { - $cart_item = $this->clean_data( $cart_item ); - } + public function insert_track_form_submit_class_name() { + ?> + + $quantity, - 'variation_id' => $variation_id, - ]; + /** + * @param WC_Product $product General information about the product added to cart. + * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc. + * + * @return void + */ + public function track_add_to_cart( $product, $add_to_cart_data ) { + $product_data = $this->clean_data( $product->get_data() ); + $added_to_cart = $this->clean_data( $add_to_cart_data ); + $cart = WC()->cart; $props = apply_filters( 'plausible_analytics_ecommerce_woocommerce_track_add_to_cart_custom_properties', [ 'props' => [ - 'item' => array_merge( $added_to_cart, $product_data ), - 'cart' => $cart_contents, + 'product_name' => $product_data[ 'name' ], + 'product_id' => $added_to_cart[ 'id' ], + 'quantity' => $added_to_cart[ 'quantity' ], + 'price' => $product_data[ 'price' ], + 'tax_class' => $product_data[ 'tax_class' ], + 'cart_total_items' => count( $cart->get_cart_contents() ), + 'cart_total' => $cart->get_total(), ], ] ); - $event_label = __( 'Add Item To Cart', 'plausible-analytics' ); $proxy = new Proxy( false ); - $proxy->do_request( $event_label, null, null, $props ); + $proxy->do_request( $this->track_add_to_cart_event_label, null, null, $props ); } /** @@ -150,26 +201,22 @@ private function clean_data( $product ) { public function track_remove_cart_item( $cart_item_key, $cart ) { $cart_contents = $cart->get_cart_contents(); $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] ); - - unset( $cart_contents[ $cart_item_key ] ); - - foreach ( $cart_contents as &$item_in_cart ) { - $item_in_cart = $this->clean_data( $item_in_cart ); - } - - $props = apply_filters( + $props = apply_filters( 'plausible_analytics_ecommerce_woocommerce_track_remove_cart_item_custom_properties', [ 'props' => [ - 'removed_item' => $item_removed_from_cart, - 'cart' => $cart_contents, + 'product_id' => $item_removed_from_cart[ 'product_id' ], + 'variation_id' => $item_removed_from_cart[ 'variation_id' ], + 'quantity' => $item_removed_from_cart[ 'quantity' ], + 'removed_item' => $item_removed_from_cart, + 'cart_total_items' => count( $cart_contents ), + 'cart_total' => $cart->get_total(), ], ] ); - $event_label = __( 'Remove Cart Item', 'plausible-analytics' ); - $proxy = new Proxy( false ); + $proxy = new Proxy( false ); - $proxy->do_request( $event_label, null, null, $props ); + $proxy->do_request( $this->track_remove_cart_item_event_label, null, null, $props ); } /** @@ -180,23 +227,21 @@ public function track_entered_checkout() { return; } - $cart = WC()->cart; - $cart_contents = $cart->get_cart_contents(); - - foreach ( $cart_contents as &$cart_item ) { - $cart_item = $this->clean_data( $cart_item ); - } - - $props = apply_filters( + $session = WC()->session; + $cart = WC()->cart; + $props = apply_filters( 'plausible_analytics_ecommerce_woocommerce_track_entered_checkout_custom_properties', [ - 'cart' => $cart_contents, + 'customer_id' => $session->get_customer_id(), + 'subtotal' => $cart->get_subtotal(), + 'shipping' => $cart->get_shipping_total(), + 'tax' => $cart->get_total_tax(), + 'total' => $cart->get_total(), ] ); - $props = wp_json_encode( $props ); - $event_label = __( 'Entered Checkout', 'plausible-analytics' ); + $props = wp_json_encode( $props ); - echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); + echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$this->track_entered_checkout_event_label', $props )" ); } /** @@ -214,47 +259,24 @@ public function track_purchase( $order_id ) { return; } - $items = $this->get_items( $order ); - $props = apply_filters( + $props = apply_filters( 'plausible_analytics_ecommerce_woocommerce_track_purchase_custom_properties', [ 'transaction_id' => $order->get_transaction_id(), - 'items' => $items, + 'order_id' => $order_id, + 'customer_id' => $order->get_customer_id(), ] ); - $props = wp_json_encode( + $props = wp_json_encode( [ 'revenue' => [ 'amount' => number_format_i18n( $order->get_total(), 2 ), 'currency' => $order->get_currency() ], 'props' => $props, ] ); - $event_label = __( 'Purchase', 'plausible-analytics' ); - echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$event_label', $props )" ); + echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$this->track_purchase_event_label', $props )" ); $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); $order->save(); } - - /** - * Returns an array of order item data. - * - * @param $order - * - * @return array - */ - private function get_items( $order ) { - $order_items = $order->get_items(); - $items = []; - - foreach ( $order_items as $id => $item ) { - $items[] = $item->get_data(); - } - - foreach ( $items as &$item ) { - $item = $this->clean_data( $item ); - } - - return $items; - } } From 93444fd9caffb8018e25f3f16a3f95b7a17320d6 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 30 May 2024 17:18:14 +0200 Subject: [PATCH 10/32] Fixed: re-factored add_to_cart_on_product_page so quantity and other properties are properly sent. --- src/Admin/Provisioning.php | 113 +++++++++++--- src/ECommerce.php | 39 ----- src/Helpers.php | 32 +++- src/Integrations.php | 57 +++++++ .../WooCommerce.php | 141 +++++++++--------- src/Plugin.php | 5 +- tests/integration/Admin/ProvisioningTest.php | 4 +- 7 files changed, 259 insertions(+), 132 deletions(-) delete mode 100644 src/ECommerce.php create mode 100644 src/Integrations.php rename src/{ECommerce => Integrations}/WooCommerce.php (57%) diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 3e71a560..2ad99435 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -11,6 +11,10 @@ use Plausible\Analytics\WP\Client; use Plausible\Analytics\WP\Client\ApiException; +use Plausible\Analytics\WP\Client\Model\GoalCreateRequestCustomEvent; +use Plausible\Analytics\WP\Helpers; +use Plausible\Analytics\WP\Integrations; +use Plausible\Analytics\WP\Integrations\WooCommerce; class Provisioning { /** @@ -21,11 +25,7 @@ class Provisioning { /** * @var string[] $custom_event_goals */ - private $custom_event_goals = [ - '404' => '404', - 'outbound-links' => 'Outbound Link: Click', - 'file-downloads' => 'File Download', - ]; + private $custom_event_goals = []; /** * @var string[] $custom_pageview_properties @@ -59,6 +59,12 @@ public function __construct( $client = null ) { $this->client = new Client(); } + $this->custom_event_goals = [ + '404' => __( '404', 'plausible-analytics' ), + 'outbound-links' => __( 'Outbound Link: Click', 'plausible-analytics' ), + 'file-downloads' => __( 'File Download', 'plausible-analytics' ), + ]; + $this->init(); } @@ -76,7 +82,8 @@ private function init() { } add_action( 'update_option_plausible_analytics_settings', [ $this, 'create_shared_link' ], 10, 2 ); - add_action( 'update_option_plausible_analytics_settings', [ $this, 'create_goals' ], 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_goals' ], 10, 2 ); add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_delete_goals' ], 11, 2 ); add_action( 'update_option_plausible_analytics_settings', [ $this, 'maybe_create_custom_properties' ], 11, 2 ); } @@ -117,7 +124,7 @@ public function create_shared_link( $old_settings, $settings ) { * @param $old_settings * @param $settings */ - public function create_goals( $old_settings, $settings ) { + public function maybe_create_goals( $old_settings, $settings ) { $enhanced_measurements = array_filter( $settings[ 'enhanced_measurements' ] ); if ( empty( $enhanced_measurements ) ) { @@ -125,7 +132,6 @@ public function create_goals( $old_settings, $settings ) { } $custom_event_keys = array_keys( $this->custom_event_goals ); - $create_request = new Client\Model\GoalCreateRequestBulkGetOrCreate(); $goals = []; foreach ( $enhanced_measurements as $measurement ) { @@ -133,20 +139,47 @@ public function create_goals( $old_settings, $settings ) { continue; // @codeCoverageIgnore } - $goals[] = new Client\Model\GoalCreateRequestCustomEvent( - [ - 'goal' => [ - 'event_name' => $this->custom_event_goals[ $measurement ], - ], - 'goal_type' => 'Goal.CustomEvent', - ] - ); + $goals[] = $this->create_request_custom_event( $this->custom_event_goals[ $measurement ] ); } + $this->create_goals( $goals ); + } + + /** + * @param string $name Event Name + * @param string $type CustomEvent|Revenue|Pageview + * @param string $currency Required if $type is Revenue + * + * @return GoalCreateRequestCustomEvent + */ + private function create_request_custom_event( $name, $type = 'CustomEvent', $currency = '' ) { + $props = [ + 'goal' => [ + 'event_name' => $name, + ], + 'goal_type' => "Goal.$type", + ]; + + if ( $type === 'Revenue' ) { + $props[ 'goal' ][ 'currency' ] = $currency; + } + + return new Client\Model\GoalCreateRequestCustomEvent( $props ); + } + + /** + * Create the goals using the API client and updates the IDs in the database. + * + * @param $goals + * + * @return void + */ + private function create_goals( $goals ) { if ( empty( $goals ) ) { return; // @codeCoverageIgnore } + $create_request = new Client\Model\GoalCreateRequestBulkGetOrCreate(); $create_request->setGoals( $goals ); $response = $this->client->create_goals( $create_request ); @@ -165,6 +198,33 @@ public function create_goals( $old_settings, $settings ) { } } + /** + * @param $old_settings + * @param $settings + * + * @return void + */ + public function maybe_create_woocommerce_goals( $old_settings, $settings ) { + if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { + return; + } + + $goals = []; + $woocommerce = new WooCommerce( false ); + + foreach ( $woocommerce->event_goals as $event_key => $event_goal ) { + if ( $event_key === 'purchase' ) { + $goals[] = $this->create_request_custom_event( $event_goal, 'Revenue', get_woocommerce_currency() ); + + continue; + } + + $goals[] = $this->create_request_custom_event( $event_goal ); + } + + $this->create_goals( $goals ); + } + /** * Delete Custom Event Goals when an Enhanced Measurement is disabled. * @@ -206,15 +266,30 @@ public function maybe_delete_goals( $old_settings, $settings ) { public function maybe_create_custom_properties( $old_settings, $settings ) { $enhanced_measurements = $settings[ 'enhanced_measurements' ]; - if ( ! in_array( 'pageview-props', $enhanced_measurements ) ) { + if ( ! Helpers::is_enhanced_measurement_enabled( 'pageview-props', $enhanced_measurements ) && + ! Helpers::is_enhanced_measurement_enabled( 'revenue', $enhanced_measurements ) ) { return; // @codeCoverageIgnore } $create_request = new Client\Model\CustomPropEnableRequestBulkEnable(); $properties = []; - foreach ( $this->custom_pageview_properties as $property ) { - $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); + /** + * Enable Custom Properties for Authors & Categories option. + */ + if ( Helpers::is_enhanced_measurement_enabled( 'pageview-props', $enhanced_measurements ) ) { + foreach ( $this->custom_pageview_properties as $property ) { + $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); + } + } + + /** + * 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 ) { + $properties[] = new Client\Model\CustomProp( [ 'custom_prop' => [ 'key' => $property ] ] ); + } } $create_request->setCustomProps( $properties ); diff --git a/src/ECommerce.php b/src/ECommerce.php deleted file mode 100644 index 588a2174..00000000 --- a/src/ECommerce.php +++ /dev/null @@ -1,39 +0,0 @@ -document.addEventListener("DOMContentLoaded", () => { %s });'; - - /** - * Build class. - */ - public function __construct() { - $this->init(); - } - - /** - * Execute Ecommerce integrations. - * - * @return void - */ - private function init() { - // WooCommerce - if ( function_exists( 'WC' ) ) { - new ECommerce\WooCommerce(); - } - - // Easy Digital Downloads - if ( function_exists( 'EDD' ) ) { - // new ECommerce\EDD(); - } - } -} diff --git a/src/Helpers.php b/src/Helpers.php index a0819788..1ceb7906 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -52,11 +52,20 @@ public static function get_filename( $local = false ) { } foreach ( [ 'outbound-links', 'file-downloads', 'tagged-events', 'revenue', 'pageview-props', 'compat', 'hash' ] as $extension ) { - if ( is_array( $settings[ 'enhanced_measurements' ] ) && in_array( $extension, $settings[ 'enhanced_measurements' ], true ) ) { + if ( self::is_enhanced_measurement_enabled( $extension ) ) { $file_name .= '.' . $extension; } } + /** + * Custom Events needs to be enabled, if Revenue Tracking is enabled and any of the available integrations are available. + */ + if ( ! self::is_enhanced_measurement_enabled( 'tagged-events' ) && + self::is_enhanced_measurement_enabled( 'revenue' ) && + ( Integrations::is_wc_active() || Integrations::is_edd_active() ) ) { + $file_name .= '.' . 'tagged-events'; + } + // Load exclusions.js if any excluded pages are set. if ( ! empty( $settings[ 'excluded_pages' ] ) ) { $file_name .= '.' . 'exclusions'; @@ -169,6 +178,27 @@ public static function get_proxy_resources() { return $resources; } + /** + * Check if a certain Enhanced Measurement is enabled. + * + * @param string $name Name of the option to check, valid values are + * 404|outbound-links|file-downloads|tagged-events|revenue|pageview-props|hash|compat. + * @param array $enhanced_measurements Allows checking against a different set of options. + * + * @return bool + */ + public static function is_enhanced_measurement_enabled( $name, $enhanced_measurements = [] ) { + if ( empty( $enhanced_measurements ) ) { + $enhanced_measurements = Helpers::get_settings()[ 'enhanced_measurements' ]; + } + + if ( ! is_array( $enhanced_measurements ) ) { + return false; + } + + return in_array( $name, $enhanced_measurements ); + } + /** * Returns the URL of the domain where Plausible Analytics is hosted: self-hosted or cloud. * diff --git a/src/Integrations.php b/src/Integrations.php new file mode 100644 index 00000000..77173600 --- /dev/null +++ b/src/Integrations.php @@ -0,0 +1,57 @@ +document.addEventListener("DOMContentLoaded", () => { %s });'; + + /** + * Build class. + */ + public function __construct() { + $this->init(); + } + + /** + * Run available integrations. + * + * @return void + */ + private function init() { + // WooCommerce + if ( self::is_wc_active() ) { + new Integrations\WooCommerce(); + } + + // Easy Digital Downloads + if ( self::is_edd_active() ) { + // new Integrations\EDD(); + } + } + + /** + * Checks if WooCommerce is installed and activated. + * + * @return bool + */ + public static function is_wc_active() { + return function_exists( 'WC' ); + } + + /** + * Checks if Easy Digital Downloads is installed and activated. + * + * @return bool + */ + public static function is_edd_active() { + return function_exists( 'EDD' ); + } +} diff --git a/src/ECommerce/WooCommerce.php b/src/Integrations/WooCommerce.php similarity index 57% rename from src/ECommerce/WooCommerce.php rename to src/Integrations/WooCommerce.php index 2c95715a..3f970064 100644 --- a/src/ECommerce/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -1,15 +1,15 @@ track_add_to_cart_event_label = __( 'Add Item To Cart', 'plausible-analytics' ); - $this->track_remove_cart_item_event_label = __( 'Remove Cart Item', 'plausible-analytics' ); - $this->track_entered_checkout_event_label = __( 'Entered Checkout', 'plausible-analytics' ); - $this->track_purchase_event_label = __( 'Purchase', 'plausible-analytics' ); - - $this->init(); + public function __construct( $init = true ) { + $this->event_goals = [ + 'add-to-cart' => __( 'Add Item To Cart', 'plausible-analytics' ), + 'remove-from-cart' => __( 'Remove Cart Item', 'plausible-analytics' ), + 'checkout' => __( 'Entered Checkout', 'plausible-analytics' ), + 'purchase' => __( 'Purchase', 'plausible-analytics' ), + ]; + + $this->init( $init ); } /** @@ -73,14 +60,21 @@ public function __construct() { * * @return void */ - private function init() { + private function init( $init ) { + if ( ! $init ) { + return; + } + + /** + * Adds required JS and classes. + */ add_action( 'wp_enqueue_scripts', [ $this, 'add_js' ], 1 ); add_filter( 'woocommerce_store_api_add_to_cart_data', [ $this, 'add_http_referer' ], 10, 2 ); - add_filter( 'woocommerce_before_add_to_cart_button', [ $this, 'insert_track_form_submit_class_name' ] ); + /** - * @todo We should use woocommerce_add_to_cart action instead, but that currently doesn't trigger on consecutive adds to the cart. Fix when resolved in WC. - * @see https://wordpress.org/support/topic/woocommerce_add_to_cart-action-isnt-triggered-on-consecutive-items/ + * Trigger tracking events. */ + add_filter( 'woocommerce_after_add_to_cart_form', [ $this, 'track_add_to_cart_on_product_page' ] ); add_filter( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); @@ -107,7 +101,7 @@ public function add_js() { } /** - * A bit of a hacky approach to ensuring the _wp_http_referer header is available to us when hitting the Proxy in @see self::track_add_to_cart() + * A bit of a hacky approach to ensure the _wp_http_referer header is available to us when hitting the Proxy in @see self::track_add_to_cart() * and @see self::track_remove_cart_item(). * * @param $add_to_cart_data @@ -130,16 +124,23 @@ public function add_http_referer( $add_to_cart_data, $request ) { * * @return void */ - public function insert_track_form_submit_class_name() { + public function track_add_to_cart_on_product_page() { + $product = wc_get_product(); ?> clean_data( $add_to_cart_data ); $cart = WC()->cart; $props = apply_filters( - 'plausible_analytics_ecommerce_woocommerce_track_add_to_cart_custom_properties', + 'plausible_analytics_woocommerce_add_to_cart_custom_properties', [ - 'props' => [ - 'product_name' => $product_data[ 'name' ], - 'product_id' => $added_to_cart[ 'id' ], - 'quantity' => $added_to_cart[ 'quantity' ], - 'price' => $product_data[ 'price' ], - 'tax_class' => $product_data[ 'tax_class' ], - 'cart_total_items' => count( $cart->get_cart_contents() ), - 'cart_total' => $cart->get_total(), - ], + 'product_name' => $product_data[ 'name' ], + 'product_id' => $added_to_cart[ 'id' ], + 'quantity' => $added_to_cart[ 'quantity' ], + 'price' => $product_data[ 'price' ], + 'tax_class' => $product_data[ 'tax_class' ], + 'cart_total_items' => count( $cart->get_cart_contents() ), + 'cart_total' => $cart->get_total( null ), ] ); $proxy = new Proxy( false ); - $proxy->do_request( $this->track_add_to_cart_event_label, null, null, $props ); + $proxy->do_request( $this->event_goals[ 'add-to-cart' ], null, null, $props ); } /** @@ -202,21 +201,19 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { $cart_contents = $cart->get_cart_contents(); $item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] ); $props = apply_filters( - 'plausible_analytics_ecommerce_woocommerce_track_remove_cart_item_custom_properties', + 'plausible_analytics_woocommerce_remove_cart_item_custom_properties', [ - 'props' => [ - 'product_id' => $item_removed_from_cart[ 'product_id' ], - 'variation_id' => $item_removed_from_cart[ 'variation_id' ], - 'quantity' => $item_removed_from_cart[ 'quantity' ], - 'removed_item' => $item_removed_from_cart, - 'cart_total_items' => count( $cart_contents ), - 'cart_total' => $cart->get_total(), - ], + 'product_id' => $item_removed_from_cart[ 'product_id' ], + 'variation_id' => $item_removed_from_cart[ 'variation_id' ], + 'quantity' => $item_removed_from_cart[ 'quantity' ], + 'removed_item' => $item_removed_from_cart, + 'cart_total_items' => count( $cart_contents ), + 'cart_total' => $cart->get_total( null ), ] ); $proxy = new Proxy( false ); - $proxy->do_request( $this->track_remove_cart_item_event_label, null, null, $props ); + $proxy->do_request( $this->event_goals[ 'remove-from-cart' ], null, null, $props ); } /** @@ -230,18 +227,21 @@ public function track_entered_checkout() { $session = WC()->session; $cart = WC()->cart; $props = apply_filters( - 'plausible_analytics_ecommerce_woocommerce_track_entered_checkout_custom_properties', + 'plausible_analytics_woocommerce_entered_checkout_custom_properties', [ - 'customer_id' => $session->get_customer_id(), - 'subtotal' => $cart->get_subtotal(), - 'shipping' => $cart->get_shipping_total(), - 'tax' => $cart->get_total_tax(), - 'total' => $cart->get_total(), + 'props' => [ + 'customer_id' => $session->get_customer_id(), + 'subtotal' => $cart->get_subtotal(), + 'shipping' => $cart->get_shipping_total(), + 'tax' => $cart->get_total_tax(), + 'total' => $cart->get_total( null ), + ], ] ); $props = wp_json_encode( $props ); + $label = $this->event_goals[ 'checkout' ]; - echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$this->track_entered_checkout_event_label', $props )" ); + echo sprintf( Integrations::SCRIPT_WRAPPER, "window.plausible( '$label', $props )" ); } /** @@ -260,7 +260,7 @@ public function track_purchase( $order_id ) { } $props = apply_filters( - 'plausible_analytics_ecommerce_woocommerce_track_purchase_custom_properties', + 'plausible_analytics_woocommerce_purchase_custom_properties', [ 'transaction_id' => $order->get_transaction_id(), 'order_id' => $order_id, @@ -273,8 +273,9 @@ public function track_purchase( $order_id ) { 'props' => $props, ] ); + $label = $this->event_goals[ 'purchase' ]; - echo sprintf( ECommerce::SCRIPT_WRAPPER, "window.plausible( '$this->track_purchase_event_label', $props )" ); + echo sprintf( Integrations::SCRIPT_WRAPPER, "window.plausible( '$label', $props )" ); $order->add_meta_data( self::PURCHASE_TRACKED_META_KEY, true ); $order->save(); diff --git a/src/Plugin.php b/src/Plugin.php index cc59b4d4..e86fdfb8 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -40,10 +40,13 @@ public function register_services() { new Admin\Provisioning(); } + if ( Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { + new Integrations(); + } + new Actions(); new Ajax(); new Compatibility(); - new ECommerce(); new Filters(); new Proxy(); new Setup(); diff --git a/tests/integration/Admin/ProvisioningTest.php b/tests/integration/Admin/ProvisioningTest.php index 1b76f002..02f226a2 100644 --- a/tests/integration/Admin/ProvisioningTest.php +++ b/tests/integration/Admin/ProvisioningTest.php @@ -45,7 +45,7 @@ public function testCreateSharedLink() { } /** - * @see Provisioning::create_goals() + * @see Provisioning::maybe_create_goals() * @throws ApiException */ public function testCreateGoals() { @@ -84,7 +84,7 @@ public function testCreateGoals() { $class = new Provisioning( $mock ); - $class->create_goals( [], $settings ); + $class->maybe_create_goals( [], $settings ); $goal_ids = get_option( 'plausible_analytics_enhanced_measurements_goal_ids' ); From c97104d95d092f0e18319e79eaeb8d206be8440f Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 31 May 2024 10:22:15 +0200 Subject: [PATCH 11/32] Fixed: is_api_token_valid() would return true when in some situations the token was invalid. --- src/Client.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Client.php b/src/Client.php index e3016ac6..38d7a1dd 100644 --- a/src/Client.php +++ b/src/Client.php @@ -71,9 +71,10 @@ public function validate_api_token() { * @return bool */ public function is_api_token_valid() { - $token = $this->api_instance->getConfig()->getPassword(); + $token = $this->api_instance->getConfig()->getPassword(); + $valid_tokens = get_transient( 'plausible_analytics_valid_token' ); - return ! empty( get_transient( 'plausible_analytics_valid_token' )[ $token ] ); + return isset( $valid_tokens[ $token ] ) && $valid_tokens[ $token ] === true; } /** From 6f3039b520b4e46796d6878341fead542ae7d63d Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 31 May 2024 10:22:43 +0200 Subject: [PATCH 12/32] Fixed: make sure get_domain() returns current options are used if current request is an AJAX save options request. --- src/Helpers.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Helpers.php b/src/Helpers.php index 1ceb7906..c0c39ebc 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -286,6 +286,17 @@ public static function download_file( $remote_file, $local_file ) { public static function get_domain() { $settings = self::get_settings(); + /** + * If this is an AJAX request, make sure the latest settings are used. + */ + if ( isset( $_POST[ 'action' ] ) && $_POST[ 'action' ] === 'plausible_analytics_save_options' ) { + $settings = json_decode( str_replace( '\\', '', $_POST[ 'options' ] ) ); + + foreach ( $settings as $setting ) { + $settings[ $setting->name ] = $setting->value; + } + } + if ( ! empty( $settings[ 'domain_name' ] ) ) { return $settings[ 'domain_name' ]; } From 91148206c571b172e0d2df89a2e744728426ec9c Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 31 May 2024 10:29:18 +0200 Subject: [PATCH 13/32] Improved: logic of previous fix should be moved to get_settings() --- src/Helpers.php | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Helpers.php b/src/Helpers.php index c0c39ebc..99f1ff27 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -99,6 +99,17 @@ public static function get_settings() { $settings = get_option( 'plausible_analytics_settings', [] ); + /** + * If this is an AJAX request, make sure the latest settings are used. + */ + if ( isset( $_POST[ 'action' ] ) && $_POST[ 'action' ] === 'plausible_analytics_save_options' ) { + $options = json_decode( str_replace( '\\', '', $_POST[ 'options' ] ) ); + + foreach ( $options as $option ) { + $settings[ $option->name ] = $option->value; + } + } + return apply_filters( 'plausible_analytics_settings', wp_parse_args( $settings, $defaults ) ); } @@ -286,17 +297,6 @@ public static function download_file( $remote_file, $local_file ) { public static function get_domain() { $settings = self::get_settings(); - /** - * If this is an AJAX request, make sure the latest settings are used. - */ - if ( isset( $_POST[ 'action' ] ) && $_POST[ 'action' ] === 'plausible_analytics_save_options' ) { - $settings = json_decode( str_replace( '\\', '', $_POST[ 'options' ] ) ); - - foreach ( $settings as $setting ) { - $settings[ $setting->name ] = $setting->value; - } - } - if ( ! empty( $settings[ 'domain_name' ] ) ) { return $settings[ 'domain_name' ]; } From 04fb33a49d601160d28c890ab7f6352f7a2af9ed Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:31:05 +0200 Subject: [PATCH 14/32] Fixed: track AJAX (i.e. non-interactivity API) add to cart events for non-block themes. --- src/Integrations/WooCommerce.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 3f970064..adef83cc 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -21,11 +21,10 @@ class WooCommerce { 'cart_total', 'cart_total_items', 'customer_id', - 'id', - 'name', 'order_id', 'price', 'product_id', + 'product_name', 'quantity', 'shipping', 'subtotal', @@ -76,6 +75,7 @@ private function init( $init ) { */ add_filter( 'woocommerce_after_add_to_cart_form', [ $this, 'track_add_to_cart_on_product_page' ] ); add_filter( 'woocommerce_store_api_validate_add_to_cart', [ $this, 'track_add_to_cart' ], 10, 2 ); + add_filter( 'woocommerce_ajax_added_to_cart', [ $this, 'track_ajax_add_to_cart' ] ); add_action( 'woocommerce_remove_cart_item', [ $this, 'track_remove_cart_item' ], 10, 2 ); add_action( 'wp_head', [ $this, 'track_entered_checkout' ] ); add_action( 'woocommerce_thankyou', [ $this, 'track_purchase' ] ); @@ -146,6 +146,25 @@ public function track_add_to_cart_on_product_page() { } /** + * Track (non-Interactivity API, i.e. AJAX) add to cart events. + * + * @param string|int $product_id ID of the product added to the cart. + * + * @return void + */ + public function track_ajax_add_to_cart( $product_id ) { + $product = wc_get_product( $product_id ); + $add_to_cart_data = [ + 'id' => $product_id, + 'quantity' => $_POST[ 'quantity' ] ?? 1, + ]; + + $this->track_add_to_cart( $product, $add_to_cart_data ); + } + + /** + * Track regular (i.e. interactivity API) add to cart events. + * * @param WC_Product $product General information about the product added to cart. * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc. * From 7a60dfa3fc342557aad37b8eb7e94956e6b726e1 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Sat, 1 Jun 2024 10:49:00 +0200 Subject: [PATCH 15/32] Fixed: ID and Product Name weren't properly tracked on product overview pages. --- src/Integrations/WooCommerce.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index adef83cc..d2511d0d 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -21,6 +21,8 @@ class WooCommerce { 'cart_total', 'cart_total_items', 'customer_id', + 'id', + 'name', 'order_id', 'price', 'product_id', From 193445feb9c285b3951d7ab8932d2f88fe899480 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Tue, 4 Jun 2024 14:16:15 +0200 Subject: [PATCH 16/32] Improved: only store valid API tokens in DB. --- src/Client.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index 38d7a1dd..c2c7a632 100644 --- a/src/Client.php +++ b/src/Client.php @@ -60,7 +60,12 @@ public function validate_api_token() { $token = $this->api_instance->getConfig()->getPassword(); $is_valid = strpos( $token, 'plausible-plugin' ) !== false && ! empty( $features->getGoals() ) && $data_domain === Helpers::get_domain(); - set_transient( 'plausible_analytics_valid_token', [ $token => $is_valid ], 86400 ); + /** + * Don't cache invalid API tokens. + */ + if ( $is_valid ) { + set_transient( 'plausible_analytics_valid_token', [ $token => true ], 86400 ); + } return $is_valid; } From ed6e0ba6beeff47dfcd5e80222b31cfa2f0e56f9 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:16:53 +0200 Subject: [PATCH 17/32] Removed order_id and customer_id from custom properties, due to GDPR implications. --- src/Integrations/WooCommerce.php | 22 ++++++++-------------- 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index d2511d0d..0abb6307 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -20,10 +20,8 @@ class WooCommerce { const CUSTOM_PROPERTIES = [ 'cart_total', 'cart_total_items', - 'customer_id', 'id', 'name', - 'order_id', 'price', 'product_id', 'product_name', @@ -245,22 +243,20 @@ public function track_entered_checkout() { return; } - $session = WC()->session; - $cart = WC()->cart; - $props = apply_filters( + $cart = WC()->cart; + $props = apply_filters( 'plausible_analytics_woocommerce_entered_checkout_custom_properties', [ 'props' => [ - 'customer_id' => $session->get_customer_id(), - 'subtotal' => $cart->get_subtotal(), - 'shipping' => $cart->get_shipping_total(), - 'tax' => $cart->get_total_tax(), - 'total' => $cart->get_total( null ), + 'subtotal' => $cart->get_subtotal(), + 'shipping' => $cart->get_shipping_total(), + 'tax' => $cart->get_total_tax(), + 'total' => $cart->get_total( null ), ], ] ); - $props = wp_json_encode( $props ); - $label = $this->event_goals[ 'checkout' ]; + $props = wp_json_encode( $props ); + $label = $this->event_goals[ 'checkout' ]; echo sprintf( Integrations::SCRIPT_WRAPPER, "window.plausible( '$label', $props )" ); } @@ -284,8 +280,6 @@ public function track_purchase( $order_id ) { 'plausible_analytics_woocommerce_purchase_custom_properties', [ 'transaction_id' => $order->get_transaction_id(), - 'order_id' => $order_id, - 'customer_id' => $order->get_customer_id(), ] ); $props = wp_json_encode( From c232c1a79d65094cbbd75bbc4c8e6cb45fdb7f2a Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:17:35 +0200 Subject: [PATCH 18/32] PHPDoc. --- src/Integrations/WooCommerce.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 0abb6307..6dde1b2c 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -120,7 +120,9 @@ public function add_http_referer( $add_to_cart_data, $request ) { } /** - * A hacky approach (with lack of a proper solution) to make sure Add To Cart events are tracked on simple product pages. + * A hacky approach (with lack of a proper solution) to make sure Add To Cart events are tracked on simple product pages. Unfortunately, cart + * information isn't available this way. + * * * @return void */ From 06c4e65d9837ad140dd19cebc1eb52d54c4039e2 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Wed, 5 Jun 2024 13:19:33 +0200 Subject: [PATCH 19/32] Removed transaction_id as well. --- src/Integrations/WooCommerce.php | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 6dde1b2c..fab3f7cb 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -278,16 +278,9 @@ public function track_purchase( $order_id ) { return; } - $props = apply_filters( - 'plausible_analytics_woocommerce_purchase_custom_properties', - [ - 'transaction_id' => $order->get_transaction_id(), - ] - ); $props = wp_json_encode( [ 'revenue' => [ 'amount' => number_format_i18n( $order->get_total(), 2 ), 'currency' => $order->get_currency() ], - 'props' => $props, ] ); $label = $this->event_goals[ 'purchase' ]; From 3b77e51091114f4d67bd9c1c7e53a463061c15ca Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:31:06 +0200 Subject: [PATCH 20/32] Ignore safety measures and untestable code. --- src/Admin/Provisioning.php | 2 +- src/Helpers.php | 2 +- src/Plugin.php | 2 +- src/Proxy.php | 6 +++++- 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 2ad99435..88891d50 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -206,7 +206,7 @@ private function create_goals( $goals ) { */ public function maybe_create_woocommerce_goals( $old_settings, $settings ) { if ( ! Helpers::is_enhanced_measurement_enabled( 'revenue', $settings[ 'enhanced_measurements' ] ) || ! Integrations::is_wc_active() ) { - return; + return; // @codeCoverageIgnore } $goals = []; diff --git a/src/Helpers.php b/src/Helpers.php index 99f1ff27..0a091686 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -204,7 +204,7 @@ public static function is_enhanced_measurement_enabled( $name, $enhanced_measure } if ( ! is_array( $enhanced_measurements ) ) { - return false; + return false; // @codeCoverageIgnore } return in_array( $name, $enhanced_measurements ); diff --git a/src/Plugin.php b/src/Plugin.php index e86fdfb8..c0124c0d 100644 --- a/src/Plugin.php +++ b/src/Plugin.php @@ -41,7 +41,7 @@ public function register_services() { } if ( Helpers::is_enhanced_measurement_enabled( 'revenue' ) ) { - new Integrations(); + new Integrations(); // @codeCoverageIgnore } new Actions(); diff --git a/src/Proxy.php b/src/Proxy.php index 2a3a8e1a..dd8b8ca9 100644 --- a/src/Proxy.php +++ b/src/Proxy.php @@ -116,7 +116,7 @@ public function do_request( $name = 'pageview', $domain = '', $url = '', $props ]; if ( ! empty( $props ) ) { - $body[ 'p' ] = $props; + $body[ 'p' ] = $props; // @codeCoverageIgnore } $request->set_body( wp_json_encode( $body ) ); @@ -189,6 +189,8 @@ private function header_exists( $global ) { * Register the API route. * * @return void + * + * @codeCoverageIgnore Because we have no way of knowing if the API works in integration tests. */ public function register_route() { register_rest_route( @@ -214,6 +216,8 @@ public function register_route() { * @param WP_REST_Request $request * * @return WP_HTTP_Response + * + * @codeCoverageIgnore */ public function force_http_response_code( $response, $server, $request ) { if ( strpos( $request->get_route(), $this->namespace ) === false ) { From 25faee6f3a868edbf18719a2c3fa27bf9bbc7bfd Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Thu, 6 Jun 2024 20:34:39 +0200 Subject: [PATCH 21/32] This code won't be tested here. --- src/Compatibility.php | 3 +++ src/Integrations.php | 3 +++ 2 files changed, 6 insertions(+) diff --git a/src/Compatibility.php b/src/Compatibility.php index 1ace0275..0dba07db 100644 --- a/src/Compatibility.php +++ b/src/Compatibility.php @@ -11,6 +11,9 @@ use Exception; +/** + * @codeCoverageIgnore Because this is to be tested in a headless browser. + */ class Compatibility { /** * A list of filters and actions to prevent our script from being manipulated by other plugins, known to cause issues. diff --git a/src/Integrations.php b/src/Integrations.php index 77173600..feab82fb 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -10,6 +10,9 @@ namespace Plausible\Analytics\WP; +/** + * @codeCoverageIgnore Because the code is very straight-forward. + */ class Integrations { const SCRIPT_WRAPPER = ''; From 6441a3539b5d474c774f1738704c88a30e35acdf Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 09:44:14 +0200 Subject: [PATCH 22/32] There's no reason to assume this would fail. --- src/Client.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Client.php b/src/Client.php index c2c7a632..dadf3cef 100644 --- a/src/Client.php +++ b/src/Client.php @@ -64,7 +64,7 @@ public function validate_api_token() { * Don't cache invalid API tokens. */ if ( $is_valid ) { - set_transient( 'plausible_analytics_valid_token', [ $token => true ], 86400 ); + set_transient( 'plausible_analytics_valid_token', [ $token => true ], 86400 ); // @codeCoverageIgnore } return $is_valid; From 845b9ae6c039f07891b4394027889d2d1a875743 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:32:19 +0200 Subject: [PATCH 23/32] Added: plausible_analytics_integrations_* filters. --- src/Integrations.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Integrations.php b/src/Integrations.php index feab82fb..97d22bd3 100644 --- a/src/Integrations.php +++ b/src/Integrations.php @@ -46,7 +46,7 @@ private function init() { * @return bool */ public static function is_wc_active() { - return function_exists( 'WC' ); + return apply_filters( 'plausible_analytics_integrations_woocommerce', function_exists( 'WC' ) ); } /** @@ -55,6 +55,6 @@ public static function is_wc_active() { * @return bool */ public static function is_edd_active() { - return function_exists( 'EDD' ); + return apply_filters( 'plausible_analytics_integrations_edd', function_exists( 'EDD' ) ); } } From 0890bdd45d8942242f342fa112f170a686166faf Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:36:20 +0200 Subject: [PATCH 24/32] Added: testGetPostSettings and expanded testGetFilename --- tests/integration/HelpersTest.php | 47 ++++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/tests/integration/HelpersTest.php b/tests/integration/HelpersTest.php index a51ffeab..69d3dcba 100644 --- a/tests/integration/HelpersTest.php +++ b/tests/integration/HelpersTest.php @@ -5,6 +5,7 @@ namespace Plausible\Analytics\Tests\Integration; +use Exception; use Plausible\Analytics\Tests\TestCase; use Plausible\Analytics\WP\Helpers; @@ -49,6 +50,7 @@ public function enableSelfHostedDomain( $settings ) { /** * @see Helpers::get_filename() + * @throws Exception */ public function testGetFilename() { add_filter( 'plausible_analytics_settings', [ $this, 'addExcludedPages' ] ); @@ -74,6 +76,16 @@ public function testGetFilename() { remove_filter( 'plausible_analytics_settings', [ $this, 'enableOutboundLinks' ] ); $this->assertEquals( 'plausible.outbound-links', $filename ); + + add_filter( 'plausible_analytics_settings', [ $this, 'enableRevenue' ] ); + add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + + $filename = Helpers::get_filename(); + + remove_filter( 'plausible_analytics_settings', [ $this, 'enableRevenue' ] ); + remove_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + + $this->assertEquals( 'plausible.revenue.tagged-events', $filename ); } /** @@ -102,10 +114,37 @@ public function enableOutboundLinks( $settings ) { return $settings; } + /** + * Enable Enhanced Measurements > Custom Events (Tagged Events) + * + * @param $settings + * + * @return mixed + */ + public function enableRevenue( $settings ) { + $settings[ 'enhanced_measurements' ] = [ 'revenue' ]; + + return $settings; + } + + /** + * @see Helpers::get_settings() + * + * @return void + */ + public function testGetPostSettings() { + $_POST[ 'action' ] = 'plausible_analytics_save_options'; + $_POST[ 'options' ] = wp_json_encode( [ [ 'name' => 'test', 'value' => 'test' ] ] ); + + $settings = Helpers::get_settings(); + + $this->assertArrayHasKey( 'test', $settings ); + } + /** * @see Helpers::get_proxy_resource() * @return void - * @throws \Exception + * @throws Exception */ public function testGetProxyResource() { $namespace = Helpers::get_proxy_resource( 'namespace' ); @@ -149,7 +188,7 @@ public function testUpdateSetting() { /** * @see Helpers::get_js_path() * @return void - * @throws \Exception + * @throws Exception */ public function testGetJsPath() { add_filter( 'plausible_analytics_settings', [ $this, 'enableProxy' ] ); @@ -166,7 +205,7 @@ public function testGetJsPath() { /** * @see Helpers::download_file() * @return void - * @throws \Exception + * @throws Exception */ public function testDownloadFile() { Helpers::download_file( 'https://plausible.io/js/plausible.js', wp_get_upload_dir()[ 'basedir' ] . '/test.js' ); @@ -221,7 +260,7 @@ public function testGetDataApiUrl() { /** * @see Helpers::get_rest_endpoint() * @return void - * @throws \Exception + * @throws Exception */ public function testGetRestEndpoint() { $endpoint = Helpers::get_rest_endpoint( false ); From 3b493b8223c774afbdf2cd4955f513d7cbd4f12f Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 10:39:20 +0200 Subject: [PATCH 25/32] Prevent duplicate test values. --- tests/integration/HelpersTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/integration/HelpersTest.php b/tests/integration/HelpersTest.php index 69d3dcba..9e39c20a 100644 --- a/tests/integration/HelpersTest.php +++ b/tests/integration/HelpersTest.php @@ -134,11 +134,11 @@ public function enableRevenue( $settings ) { */ public function testGetPostSettings() { $_POST[ 'action' ] = 'plausible_analytics_save_options'; - $_POST[ 'options' ] = wp_json_encode( [ [ 'name' => 'test', 'value' => 'test' ] ] ); + $_POST[ 'options' ] = wp_json_encode( [ [ 'name' => 'post_test', 'value' => 'post_test' ] ] ); $settings = Helpers::get_settings(); - $this->assertArrayHasKey( 'test', $settings ); + $this->assertArrayHasKey( 'post_test', $settings ); } /** From 4bea53484d81ce0b55d801ec4a6c4948a1aa5c52 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 11:08:22 +0200 Subject: [PATCH 26/32] Added: testMaybeCreateWooCommerceGoals() --- tests/integration/Admin/ProvisioningTest.php | 68 +++++++++++++++++++- 1 file changed, 67 insertions(+), 1 deletion(-) diff --git a/tests/integration/Admin/ProvisioningTest.php b/tests/integration/Admin/ProvisioningTest.php index 02f226a2..2a70b078 100644 --- a/tests/integration/Admin/ProvisioningTest.php +++ b/tests/integration/Admin/ProvisioningTest.php @@ -12,6 +12,7 @@ use Plausible\Analytics\WP\Client\Model\Goal; use Plausible\Analytics\WP\Client\Model\GoalPageviewAllOfGoal; use Plausible\Analytics\WP\Helpers; +use function Brain\Monkey\Functions\when; class ProvisioningTest extends TestCase { /** @@ -49,7 +50,6 @@ public function testCreateSharedLink() { * @throws ApiException */ public function testCreateGoals() { - $settings = []; $settings[ 'enhanced_measurements' ] = [ '404', 'outbound-links', @@ -92,5 +92,71 @@ public function testCreateGoals() { $this->assertArrayHasKey( 111, $goal_ids ); $this->assertArrayHasKey( 222, $goal_ids ); $this->assertArrayHasKey( 333, $goal_ids ); + + delete_option( 'plausible_analytics_enhanced_measurements_goal_ids' ); + } + + /** + * @see Provisioning::maybe_create_woocommerce_goals() + * @return void + * @throws ApiException + */ + public function testCreateWooCommerceGoals() { + $settings = [ + 'enhanced_measurements' => [ + 'revenue', + ], + ]; + $mock = $this->getMockBuilder( Client::class )->onlyMethods( [ 'create_goals' ] )->getMock(); + $goals_array = [ + new Goal( + [ + 'goal' => new GoalPageviewAllOfGoal( [ 'display_name' => 'Add Item To Cart', 'id' => 112, 'path' => null ] ), + 'goal_type' => 'Goal.CustomEvent', + ] + ), + new Goal( + [ + 'goal' => new GoalPageviewAllOfGoal( [ 'display_name' => 'Remove Cart Item', 'id' => 223, 'path' => null ] ), + 'goal_type' => 'Goal.CustomEvent', + ] + ), + new Goal( + [ + 'goal' => new GoalPageviewAllOfGoal( [ 'display_name' => 'Entered Checkout', 'id' => 334, 'path' => null ] ), + 'goal_type' => 'Goal.CustomEvent', + ] + ), + new Goal( + [ + 'goal' => new GoalPageviewAllOfGoal( [ 'display_name' => 'Purchase', 'id' => 445, 'path' => null ] ), + 'goal_type' => 'Goal.Revenue', + ] + ), + ]; + $goals = new Client\Model\GoalListResponse(); + + $goals->setGoals( $goals_array ); + $goals->setMeta( new Client\Model\GoalListResponseMeta() ); + $mock->method( 'create_goals' )->willReturn( $goals ); + + $class = new Provisioning( $mock ); + + add_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + when( 'get_woocommerce_currency' )->justReturn( 'EUR' ); + + $class->maybe_create_woocommerce_goals( [], $settings ); + + remove_filter( 'plausible_analytics_integrations_woocommerce', '__return_true' ); + + $goal_ids = get_option( 'plausible_analytics_enhanced_measurements_goal_ids' ); + + $this->assertCount( 4, $goal_ids ); + $this->assertArrayHasKey( 112, $goal_ids ); + $this->assertArrayHasKey( 223, $goal_ids ); + $this->assertArrayHasKey( 334, $goal_ids ); + $this->assertArrayHasKey( 445, $goal_ids ); + + delete_option( 'plausible_analytics_enhanced_measurements_goal_ids' ); } } From e998df2a68d43c79f3a6a1239bb838cddf556e8f Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:25:35 +0200 Subject: [PATCH 27/32] Added: WooComerceTest --- src/Integrations/WooCommerce.php | 26 +++++- .../Integrations/WooCommerceTest.php | 81 +++++++++++++++++++ 2 files changed, 103 insertions(+), 4 deletions(-) create mode 100644 tests/integration/Integrations/WooCommerceTest.php diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index fab3f7cb..d8dc0e1d 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -42,6 +42,8 @@ class WooCommerce { /** * Build class. + * + * @codeCoverageIgnore */ public function __construct( $init = true ) { $this->event_goals = [ @@ -58,6 +60,8 @@ public function __construct( $init = true ) { * Filter and action hooks. * * @return void + * + * @codeCoverageIgnore */ private function init( $init ) { if ( ! $init ) { @@ -89,7 +93,7 @@ private function init( $init ) { public function add_js() { // Causes errors in checkout and isn't needed either way. if ( is_checkout() ) { - return; + return; // @codeCoverageIgnore } wp_enqueue_script( @@ -108,6 +112,8 @@ public function add_js() { * @param $request * * @return mixed + * + * @codeCoverageIgnore */ public function add_http_referer( $add_to_cart_data, $request ) { $http_referer = $request->get_param( '_wp_http_referer' ); @@ -123,8 +129,9 @@ public function add_http_referer( $add_to_cart_data, $request ) { * A hacky approach (with lack of a proper solution) to make sure Add To Cart events are tracked on simple product pages. Unfortunately, cart * information isn't available this way. * - * * @return void + * + * @codeCoverageIgnore Because we're not testing JS here. */ public function track_add_to_cart_on_product_page() { $product = wc_get_product(); @@ -153,6 +160,8 @@ public function track_add_to_cart_on_product_page() { * @param string|int $product_id ID of the product added to the cart. * * @return void + * + * @codeCoverageIgnore Because this function returns nothing. */ public function track_ajax_add_to_cart( $product_id ) { $product = wc_get_product( $product_id ); @@ -171,6 +180,8 @@ public function track_ajax_add_to_cart( $product_id ) { * @param array $add_to_cart_data Cart data for the product added to the cart, e.g. quantity, variation ID, etc. * * @return void + * + * @codeCoverageIgnore Because this function returns nothing. */ public function track_add_to_cart( $product, $add_to_cart_data ) { $product_data = $this->clean_data( $product->get_data() ); @@ -199,6 +210,8 @@ public function track_add_to_cart( $product, $add_to_cart_data ) { * @param array $product Product Data. * * @return mixed + * + * @codeCoverageIgnore Because it can't be tested. */ private function clean_data( $product ) { foreach ( $product as $key => $value ) { @@ -217,6 +230,8 @@ private function clean_data( $product ) { * @param WC_Cart $cart Instance of the current cart. * * @return void + * + * @codeCoverageIgnore because we can't test XHR requests here. */ public function track_remove_cart_item( $cart_item_key, $cart ) { $cart_contents = $cart->get_cart_contents(); @@ -240,12 +255,15 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { /** * @return void */ - public function track_entered_checkout() { + public function track_entered_checkout( $cart = null ) { if ( ! is_checkout() ) { return; } - $cart = WC()->cart; + if ( ! $cart ) { + $cart = WC()->cart; + } + $props = apply_filters( 'plausible_analytics_woocommerce_entered_checkout_custom_properties', [ diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php new file mode 100644 index 00000000..ca72b456 --- /dev/null +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -0,0 +1,81 @@ + WooCommerce + */ + +namespace Plausible\Analytics\Tests\Integration; + +use Plausible\Analytics\Tests\TestCase; +use Plausible\Analytics\WP\Integrations\WooCommerce; +use function Brain\Monkey\Functions\when; + +class WooCommerceTest extends TestCase { + /** + * @see WooCommerce::add_js() + */ + public function testAddJs() { + when( 'is_checkout' )->justReturn( false ); + + $class = new WooCommerce( false ); + + $class->add_js(); + + $wp_scripts = wp_scripts(); + + $this->assertTrue( in_array( 'plausible-woocommerce-compatibility', $wp_scripts->queue ) ); + } + + /** + * @see WooCommerce::track_entered_checkout() + * @return void + */ + public function testTrackEnteredCheckout() { + when( 'is_checkout' )->justReturn( true ); + + $mock = $this->getMockBuilder( 'WC_Cart' )->setMethods( + [ + 'get_subtotal', + 'get_shipping_total', + 'get_total_tax', + 'get_total', + ] + )->getMock(); + + $mock->method( 'get_subtotal' )->willReturn( 10 ); + $mock->method( 'get_shipping_total' )->willReturn( 5 ); + $mock->method( 'get_total_tax' )->willReturn( 1 ); + $mock->method( 'get_total' )->willReturn( "16.00" ); + + $class = new WooCommerce( false ); + + $this->expectOutputContains( '{"props":{"subtotal":10,"shipping":5,"tax":1,"total":"16.00"}}' ); + + $class->track_entered_checkout( $mock ); + } + + /** + * @see WooCommerce::track_purchase() + * @return void + */ + public function testTrackPurchase() { + $class = new WooCommerce( false ); + $mock = $this->getMockBuilder( 'WC_Order' )->setMethods( + [ + 'get_meta', + 'get_total', + 'get_currency', + 'add_meta_data', + 'save', + ] + )->getMock(); + $mock->method( 'get_meta' )->willReturn( false ); + $mock->method( 'get_total' )->willReturn( 10 ); + $mock->method( 'get_currency' )->willReturn( 'EUR' ); + + when( 'wc_get_order' )->justReturn( $mock ); + + $this->expectOutputContains( '{"revenue":{"amount":"10.00","currency":"EUR"}}' ); + + $class->track_purchase( 1 ); + } +} From bf9a776b94c2b1d1562ece18a4adf17316b03a9d Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:27:56 +0200 Subject: [PATCH 28/32] PHPDoc. --- src/Integrations/WooCommerce.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index d8dc0e1d..9c0fddd0 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -161,7 +161,7 @@ public function track_add_to_cart_on_product_page() { * * @return void * - * @codeCoverageIgnore Because this function returns nothing. + * @codeCoverageIgnore Because we can't test XHR requests here. */ public function track_ajax_add_to_cart( $product_id ) { $product = wc_get_product( $product_id ); @@ -181,7 +181,7 @@ public function track_ajax_add_to_cart( $product_id ) { * * @return void * - * @codeCoverageIgnore Because this function returns nothing. + * @codeCoverageIgnore Because we can't test XHR requests here. */ public function track_add_to_cart( $product, $add_to_cart_data ) { $product_data = $this->clean_data( $product->get_data() ); From 913ffa83c7659de6ba7d6a21d382c65a7c5454cf Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:28:19 +0200 Subject: [PATCH 29/32] PHPDoc. --- src/Integrations/WooCommerce.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 9c0fddd0..309a1bd0 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -113,7 +113,7 @@ public function add_js() { * * @return mixed * - * @codeCoverageIgnore + * @codeCoverageIgnore Because there's nothing to test here. */ public function add_http_referer( $add_to_cart_data, $request ) { $http_referer = $request->get_param( '_wp_http_referer' ); From 0197831ad5b231552e7e2c2cf16953f0fd12c284 Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:31:13 +0200 Subject: [PATCH 30/32] Ignore AddJS --- src/Integrations/WooCommerce.php | 2 ++ .../integration/Integrations/WooCommerceTest.php | 15 --------------- 2 files changed, 2 insertions(+), 15 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 309a1bd0..4146d3fd 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -89,6 +89,8 @@ private function init( $init ) { * Enqueue required JS in frontend. * * @return void + * + * @codeCoverageIgnore Because there's nothing to test here. */ public function add_js() { // Causes errors in checkout and isn't needed either way. diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php index ca72b456..5666ec70 100644 --- a/tests/integration/Integrations/WooCommerceTest.php +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -10,21 +10,6 @@ use function Brain\Monkey\Functions\when; class WooCommerceTest extends TestCase { - /** - * @see WooCommerce::add_js() - */ - public function testAddJs() { - when( 'is_checkout' )->justReturn( false ); - - $class = new WooCommerce( false ); - - $class->add_js(); - - $wp_scripts = wp_scripts(); - - $this->assertTrue( in_array( 'plausible-woocommerce-compatibility', $wp_scripts->queue ) ); - } - /** * @see WooCommerce::track_entered_checkout() * @return void From 698b5927126855934a424b726857d0868fb97b0c Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:35:32 +0200 Subject: [PATCH 31/32] No need to modify source code. --- src/Integrations/WooCommerce.php | 6 ++---- tests/integration/Integrations/WooCommerceTest.php | 6 +++++- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index 4146d3fd..d0b5c6b9 100644 --- a/src/Integrations/WooCommerce.php +++ b/src/Integrations/WooCommerce.php @@ -257,14 +257,12 @@ public function track_remove_cart_item( $cart_item_key, $cart ) { /** * @return void */ - public function track_entered_checkout( $cart = null ) { + public function track_entered_checkout() { if ( ! is_checkout() ) { return; } - if ( ! $cart ) { - $cart = WC()->cart; - } + $cart = WC()->cart; $props = apply_filters( 'plausible_analytics_woocommerce_entered_checkout_custom_properties', diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php index 5666ec70..ce59fe0b 100644 --- a/tests/integration/Integrations/WooCommerceTest.php +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -31,11 +31,15 @@ public function testTrackEnteredCheckout() { $mock->method( 'get_total_tax' )->willReturn( 1 ); $mock->method( 'get_total' )->willReturn( "16.00" ); + $woo_mock = $this->getMockBuilder( 'WooCommerce' )->getMock(); + $woo_mock->cart = $mock; + when( 'WC' )->justReturn( $woo_mock ); + $class = new WooCommerce( false ); $this->expectOutputContains( '{"props":{"subtotal":10,"shipping":5,"tax":1,"total":"16.00"}}' ); - $class->track_entered_checkout( $mock ); + $class->track_entered_checkout(); } /** From 98f360f5e9242af9f49e529ef33e64ae4937c28d Mon Sep 17 00:00:00 2001 From: Dan0sz <18595395+Dan0sz@users.noreply.github.com> Date: Fri, 7 Jun 2024 12:35:58 +0200 Subject: [PATCH 32/32] Minor re-factor for readability. --- tests/integration/Integrations/WooCommerceTest.php | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php index ce59fe0b..37e97985 100644 --- a/tests/integration/Integrations/WooCommerceTest.php +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -17,7 +17,7 @@ class WooCommerceTest extends TestCase { public function testTrackEnteredCheckout() { when( 'is_checkout' )->justReturn( true ); - $mock = $this->getMockBuilder( 'WC_Cart' )->setMethods( + $cart_mock = $this->getMockBuilder( 'WC_Cart' )->setMethods( [ 'get_subtotal', 'get_shipping_total', @@ -26,13 +26,13 @@ public function testTrackEnteredCheckout() { ] )->getMock(); - $mock->method( 'get_subtotal' )->willReturn( 10 ); - $mock->method( 'get_shipping_total' )->willReturn( 5 ); - $mock->method( 'get_total_tax' )->willReturn( 1 ); - $mock->method( 'get_total' )->willReturn( "16.00" ); + $cart_mock->method( 'get_subtotal' )->willReturn( 10 ); + $cart_mock->method( 'get_shipping_total' )->willReturn( 5 ); + $cart_mock->method( 'get_total_tax' )->willReturn( 1 ); + $cart_mock->method( 'get_total' )->willReturn( "16.00" ); $woo_mock = $this->getMockBuilder( 'WooCommerce' )->getMock(); - $woo_mock->cart = $mock; + $woo_mock->cart = $cart_mock; when( 'WC' )->justReturn( $woo_mock ); $class = new WooCommerce( false );