diff --git a/readme.txt b/readme.txt index f7c0195d..0fbb38f4 100644 --- a/readme.txt +++ b/readme.txt @@ -47,9 +47,13 @@ Plausible is lightweight analytics. Our script is 45 times smaller than Google A Plausible is privacy-friendly analytics. All the site measurement is carried out absolutely anonymously. Cookies are not used and no personal data is collected. There are no persistent identifiers. No cross-site or cross-device tracking either. Your site data is not used for any other purposes. All visitor data is exclusively processed with servers owned and operated by European companies and it never leaves the EU. -### Track goal conversions, revenue and campaigns +### Track events and marketing campaigns -Plausible is useful. Segment your audience by any metric you click on. Answer the important questions about your visitors, content and referral sources. Analyze paid campaigns using UTM parameters. Track WooCommerce revenue, outbound link clicks, file downloads and 404 error pages. Create custom events with custom dimensions to track conversions and attribution. Increase conversions using funnel analysis. +Plausible is useful. Segment your audience by any metric you click on. Answer the important questions about your visitors, content and referral sources. Analyze paid campaigns using UTM parameters. Track site search terms, outbound link clicks, file downloads, 404 error pages, post authors, post categories and custom taxonomies without manually configuring anything or writing any code. + +### Built-in WooCommerce analytics + +Plausible provides an automatic WooCommerce analytics solution to track conversions, revenue and attribution. Activities tracked include adding to cart, removing from cart, entering checkout and completing a purchase. A purchase funnel looking at the user journey from viewing a product to making a purchase is enabled to help you see the drop-off rates between the different steps, understand your cart abandonment rate and increase your conversions. ### Invite team members and share your dashboard @@ -57,18 +61,18 @@ Plausible is shareable. Your stats are private by default but you can choose to ### Transparent and open source software -Plausible is open source analytics. Our source code is available and accessible on GitHub so anyone can read it, inspect it and review it to verify that our actions match with our words. We welcome feedback and have a public roadmap. If you're happy to manage your own infrastructure, you can self-host Plausible too. +Plausible is open source analytics. Our source code is available and accessible on GitHub so anyone can read it, inspect it and review it to verify that our actions match with our words. We welcome feedback and have a public roadmap. If you're happy to manage your own infrastructure, you can self-host Plausible too. ## Features * Our product is updated several times per week and with our WordPress plugin you always have access to all the latest features * Automatically includes tracking code in the header of your site * Simple plugin settings page with easy options and an onboarding guide -* Get more accurate stats by running the Plausible script as a first-party connection from your domain name +* Get more accurate stats and count those who use adblockers by running the Plausible script as a first-party connection from your domain name * View your Plausible stats directly in your WordPress dashboard (you can grant access to other user roles too) * Tracking of admin users is disabled by default (you can also disable tracking of other user roles) -* Enable ecommerce revenue, file downloads, external link clicks and 404 error pages tracking -* Enable automated tracking of post authors and post categories for better content analysis +* Enable WooCommerce revenue, file downloads, external link clicks, site search terms and 404 error pages tracking +* Enable automated tracking of post authors, post categories and custom taxonomies for better content analysis * Custom events and custom dimensions can be setup using CSS class names directly in the WordPress editor * Integrate with Google Search Console so you can see search queries people use to find your site in Google's search results * Import your historical Google Analytics stats diff --git a/src/Admin/Provisioning.php b/src/Admin/Provisioning.php index 12cee82d..8889770f 100644 --- a/src/Admin/Provisioning.php +++ b/src/Admin/Provisioning.php @@ -225,7 +225,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/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; 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/Helpers.php b/src/Helpers.php index 524d17cb..34c61b1e 100644 --- a/src/Helpers.php +++ b/src/Helpers.php @@ -213,7 +213,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/Integrations.php b/src/Integrations.php index 77173600..97d22bd3 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 = ''; @@ -43,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' ) ); } /** @@ -52,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' ) ); } } diff --git a/src/Integrations/WooCommerce.php b/src/Integrations/WooCommerce.php index fab3f7cb..d0b5c6b9 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 ) { @@ -85,11 +89,13 @@ 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. if ( is_checkout() ) { - return; + return; // @codeCoverageIgnore } wp_enqueue_script( @@ -108,6 +114,8 @@ public function add_js() { * @param $request * * @return mixed + * + * @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' ); @@ -123,8 +131,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 +162,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 we can't test XHR requests here. */ public function track_ajax_add_to_cart( $product_id ) { $product = wc_get_product( $product_id ); @@ -171,6 +182,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 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() ); @@ -199,6 +212,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 +232,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(); @@ -245,7 +262,8 @@ public function track_entered_checkout() { return; } - $cart = WC()->cart; + $cart = WC()->cart; + $props = apply_filters( 'plausible_analytics_woocommerce_entered_checkout_custom_properties', [ 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 ) { 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' ); } } diff --git a/tests/integration/HelpersTest.php b/tests/integration/HelpersTest.php index a51ffeab..9e39c20a 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' => 'post_test', 'value' => 'post_test' ] ] ); + + $settings = Helpers::get_settings(); + + $this->assertArrayHasKey( 'post_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 ); diff --git a/tests/integration/Integrations/WooCommerceTest.php b/tests/integration/Integrations/WooCommerceTest.php new file mode 100644 index 00000000..37e97985 --- /dev/null +++ b/tests/integration/Integrations/WooCommerceTest.php @@ -0,0 +1,70 @@ + 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::track_entered_checkout() + * @return void + */ + public function testTrackEnteredCheckout() { + when( 'is_checkout' )->justReturn( true ); + + $cart_mock = $this->getMockBuilder( 'WC_Cart' )->setMethods( + [ + 'get_subtotal', + 'get_shipping_total', + 'get_total_tax', + 'get_total', + ] + )->getMock(); + + $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 = $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(); + } + + /** + * @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 ); + } +}