Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,11 @@ Please make sure you make a backup of your database before updating any version

== Changelog ==

= 2.6.4 =
* Fixed: for user roles that aren't tracked (administrators by default), some integrations still loaded the tracker without initializing it, which caused a "window.plausible is not a function" error, e.g. on search results, 404 and checkout pages. They no longer load anything for those roles.
* Fixed: events sent from the server (WooCommerce and Easy Digital Downloads add to cart, remove from cart and EDD checkout, and Contact Form 7 and Gravity Forms submissions) were also sent for user roles that aren't tracked.
* Changed: logged-in users without a role on the site (e.g. multisite super admins visiting a subsite) are no longer tracked, as none of their roles is enabled under "Track analytics for user roles". Previously they were tracked like logged-out visitors.

= 2.6.3 =
* Improved: the tracker script now loads asynchronously (like Plausible Analytics' own snippet) instead of deferred, so it can no longer hold up the rest of the page when it can't be reached.
* Added: Hummingbird compatibility. Its Asset Optimization and Delay JavaScript no longer touch the plugin's scripts, which could cause a "plausible is not a function" error (e.g. on search results pages).
Expand Down
17 changes: 8 additions & 9 deletions src/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ private function init() {
* @return void
*/
public function maybe_enqueue_cloaked_affiliate_links_assets() {
if ( Helpers::is_cloaked_affiliate_links_enabled() && Helpers::main_script_is_registered() ) {
if ( Helpers::is_cloaked_affiliate_links_enabled() && Helpers::main_script_is_initialized() ) {
wp_enqueue_script(
'plausible-affiliate-links',
PLAUSIBLE_ANALYTICS_PLUGIN_URL . 'assets/dist/js/plausible-affiliate-links.js',
Expand All @@ -57,7 +57,7 @@ public function maybe_enqueue_cloaked_affiliate_links_assets() {
public function maybe_enqueue_four_o_four_script() {
$is_404 = apply_filters( 'plausible_analytics_is_404', is_404() );

if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::FOUR_O_FOUR ) && $is_404 && Helpers::main_script_is_registered() ) {
if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::FOUR_O_FOUR ) && $is_404 && Helpers::main_script_is_initialized() ) {
$data = wp_json_encode(
[
'props' => [
Expand Down Expand Up @@ -88,9 +88,8 @@ public function maybe_enqueue_four_o_four_script() {
* @throws \Exception
*/
public function maybe_enqueue_main_script() {
$settings = Helpers::get_settings();
$user_role = Helpers::get_user_role();
$url = $this->get_js_url( true );
$settings = Helpers::get_settings();
$url = $this->get_js_url( true );

if ( ! $url ) {
echo '<!-- ' . __( 'Please enter your plugin token to start using Plausible Analytics.', 'plausible-analytics' ) . " -->\n";
Expand All @@ -104,9 +103,9 @@ public function maybe_enqueue_main_script() {
wp_register_script( 'plausible-analytics', $url, [], null, apply_filters( 'plausible_load_js_in_footer', false ) );

/**
* Bail if tracked_user_roles is empty (which means no roles should be tracked) or if the current role should not be tracked.
* Bail if the current user's role should not be tracked.
*/
if ( ( ! empty( $user_role ) && ! isset( $settings['tracked_user_roles'] ) ) || ( ! empty( $user_role ) && ! in_array( $user_role, $settings['tracked_user_roles'], true ) ) ) {
if ( ! Helpers::is_current_user_tracked() ) {
return; // @codeCoverageIgnore
}

Expand Down Expand Up @@ -148,7 +147,7 @@ protected function get_js_url( bool $local = false ) {
* @return void
*/
public function maybe_enqueue_query_params_script() {
if ( Helpers::is_query_params_enabled() && Helpers::main_script_is_registered() ) {
if ( Helpers::is_query_params_enabled() && Helpers::main_script_is_initialized() ) {
$props = [];
$query_params = Helpers::get_settings()['query_params'] ?? [];

Expand Down Expand Up @@ -183,7 +182,7 @@ public function maybe_enqueue_query_params_script() {
public function maybe_enqueue_search_queries_script() {
$is_search = apply_filters( 'plausible_analytics_is_search', is_search() );

if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::SEARCH_QUERIES ) && $is_search && Helpers::main_script_is_registered() ) {
if ( EnhancedMeasurements::is_enabled( EnhancedMeasurements::SEARCH_QUERIES ) && $is_search && Helpers::main_script_is_initialized() ) {
global $wp_query;

$search_source = isset( $_REQUEST['search_source'] ) ? sanitize_text_field( $_REQUEST['search_source'] ) : wp_get_referer();
Expand Down
51 changes: 48 additions & 3 deletions src/Helpers.php
Original file line number Diff line number Diff line change
Expand Up @@ -827,13 +827,32 @@ public static function get_multilang_plugin_name() {
* @return string
*/
public static function get_user_role() {
global $current_user;

$user_roles = $current_user->roles;
// wp_get_current_user() makes sure the current user is determined, e.g. in REST API requests.
$user_roles = (array) wp_get_current_user()->roles;

return array_shift( $user_roles );
}

/**
* Checks whether the current visitor should be tracked: logged-out visitors always are, logged-in users only when
* their role is enabled under "Track analytics for user roles". A logged-in user without a role on this site (e.g. a
* multisite super admin visiting a subsite) has no role that's enabled, so isn't tracked.
*
* @since 2.6.4
*
* @return bool
*/
public static function is_current_user_tracked() {
if ( ! is_user_logged_in() ) {
return true;
}

$user_role = static::get_user_role();
$tracked_user_roles = static::get_settings()['tracked_user_roles'] ?? [];

return ! empty( $user_role ) && in_array( $user_role, (array) $tracked_user_roles, true );
}

/**
* Wrapper to check if the Cloaked Affiliate Links option contains any values.
*
Expand Down Expand Up @@ -886,12 +905,38 @@ public static function is_query_params_enabled( $settings = [] ) {
/**
* Checks if the main Plausible Analytics script is registered.
*
* The script is registered for every visitor, also for user roles that aren't tracked, so to decide whether to add
* tracking code, use @see self::main_script_is_initialized().
*
* @return bool
*/
public static function main_script_is_registered() {
return wp_script_is( 'plausible-analytics', 'registered' );
}

/**
* Checks if the main Plausible Analytics script is enqueued and initialized, i.e. the current visitor is tracked.
*
* Tracking code must only be added when it is: for a user role that isn't tracked, the script is registered but not
* enqueued, and neither is its inline plausible.init() call. A script that depends on it would still pull the tracker
* onto the page, which then leaves window.plausible an object instead of a function, so any call to it throws
* "window.plausible is not a function".
*
* wp_script_is( 'plausible-analytics', 'enqueued' ) isn't enough: it's also true while any enqueued script depends on
* it, e.g. another plugin's. So check that it's in the queue itself and that @see Assets::maybe_enqueue_main_script()
* added the plausible.init() call, which it only does for visitors that are tracked.
*
* @since 2.6.4
*
* @return bool
*/
public static function main_script_is_initialized() {
$scripts = wp_scripts();
$after = (array) $scripts->get_data( 'plausible-analytics', 'after' );

return in_array( 'plausible-analytics', $scripts->queue, true ) && strpos( implode( "\n", $after ), 'plausible.init(' ) !== false;
}

/**
* Translates a URL slug, e.g., WooCommerce's product base, to the given language.
*
Expand Down
21 changes: 21 additions & 0 deletions src/Integrations/EDD.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
namespace Plausible\Analytics\WP\Integrations;

use Plausible\Analytics\WP\EnhancedMeasurements;
use Plausible\Analytics\WP\Helpers;
use Plausible\Analytics\WP\Integrations;
use Plausible\Analytics\WP\Proxy;

Expand Down Expand Up @@ -84,6 +85,11 @@ private function init( $init ) {
* @codeCoverageIgnore Because we can't test XHR requests here.
*/
public function track_add_to_cart( $download_id, $options, $items ) {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

$download = new \EDD_Download( $download_id );

if ( $download->ID === 0 ) {
Expand Down Expand Up @@ -126,6 +132,11 @@ function ( $item ) use ( $download_id ) {
* @codeCoverageIgnore Because we can't test XHR requests here.
*/
public function track_remove_cart_item( $key ) {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

$cart_contents = edd_get_cart_contents();
$item_removed_from_cart = $cart_contents[ $key ] ?? [];
$product = null;
Expand Down Expand Up @@ -170,6 +181,11 @@ public function track_remove_cart_item( $key ) {
* @codeCoverageIgnore Because we can't test XHR requests here.
*/
public function track_entered_checkout() {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

// Just to make sure we're where we're supposed to be.
if ( ! edd_is_checkout() ) {
return;
Expand Down Expand Up @@ -203,6 +219,11 @@ public function track_purchase() {
return; // @codeCoverageIgnore
}

// Not for user roles that aren't tracked: window.plausible isn't initialized for them. @see Helpers::main_script_is_initialized()
if ( ! Helpers::main_script_is_initialized() ) {
return;
}

$session = edd_get_purchase_session();
$order = null;

Expand Down
7 changes: 6 additions & 1 deletion src/Integrations/FormSubmit.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ private function init() {
* @codeCoverageIgnore because there's nothing to test here.
*/
public function add_js() {
if ( ! Helpers::main_script_is_registered() ) {
if ( ! Helpers::main_script_is_initialized() ) {
return;
}

Expand Down Expand Up @@ -107,6 +107,11 @@ public function maybe_track_submission( $result, $tags ) {
* @codeCoverageIgnore because we can't test XHR requests here.
*/
private function track_submission( $uri ) {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

$proxy = new Proxy( false );

$proxy->do_request(
Expand Down
21 changes: 21 additions & 0 deletions src/Integrations/WooCommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

use Plausible\Analytics\WP\Admin\Provisioning;
use Plausible\Analytics\WP\EnhancedMeasurements;
use Plausible\Analytics\WP\Helpers;
use Plausible\Analytics\WP\Integrations;
use Plausible\Analytics\WP\Proxy;
use WC_Cart;
Expand Down Expand Up @@ -162,6 +163,11 @@ public function track_ajax_add_to_cart( $product_id ) {
* @codeCoverageIgnore Because we can't test XHR requests here.
*/
public function track_add_to_cart( $product, $add_to_cart_data ) {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

if ( ! $product ) {
return;
}
Expand Down Expand Up @@ -273,6 +279,11 @@ public function track_entered_checkout() {
return; // @codeCoverageIgnore
}

// Not for user roles that aren't tracked: window.plausible isn't initialized for them. @see Helpers::main_script_is_initialized()
if ( ! Helpers::main_script_is_initialized() ) {
return;
}

$cart = $this->get_wc_cart();
$props = apply_filters(
'plausible_analytics_woocommerce_entered_checkout_custom_properties',
Expand Down Expand Up @@ -301,6 +312,11 @@ public function track_entered_checkout() {
* @return void
*/
public function track_purchase( $order_id ) {
// Not for user roles that aren't tracked: window.plausible isn't initialized for them. @see Helpers::main_script_is_initialized()
if ( ! Helpers::main_script_is_initialized() ) {
return;
}

$order = wc_get_order( $order_id );
$is_tracked = $order->get_meta( Integrations::PURCHASE_TRACKED_META_KEY );

Expand Down Expand Up @@ -337,6 +353,11 @@ public function track_purchase( $order_id ) {
* @codeCoverageIgnore because we can't test XHR requests here.
*/
public function track_remove_cart_item( $cart_item_key, $cart ) {
// Not for user roles that aren't tracked. @see Helpers::is_current_user_tracked()
if ( ! Helpers::is_current_user_tracked() ) {
return;
}

$cart_contents = $cart->get_cart_contents();
$item_removed_from_cart = $this->clean_data( $cart_contents[ $cart_item_key ] ?? [] );
$product = null;
Expand Down
29 changes: 29 additions & 0 deletions tests/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,35 @@ function ( $caps ) use ( $cap ) {
);
}

/**
* Enqueue and initialize the main script, like Assets::maybe_enqueue_main_script() does for a visitor that's tracked.
*
* @return void
*/
public function enqueueMainScript() {
wp_register_script( 'plausible-analytics', 'https://plausible.test/js/pa-test.js', [], null );
wp_enqueue_script( 'plausible-analytics' );
wp_add_inline_script( 'plausible-analytics', 'plausible.init({});' );
}

/**
* Leave the main script registered but not enqueued, like Assets::maybe_enqueue_main_script() does for a user role
* that isn't tracked.
*
* @return void
*/
public function dequeueMainScript() {
wp_register_script( 'plausible-analytics', 'https://plausible.test/js/pa-test.js', [], null );
wp_dequeue_script( 'plausible-analytics' );

// A script counts as enqueued while an enqueued script depends on it, e.g. one left behind by another test.
foreach ( wp_scripts()->queue as $handle ) {
if ( in_array( 'plausible-analytics', (array) ( wp_scripts()->registered[ $handle ]->deps ?? [] ), true ) ) {
wp_dequeue_script( $handle );
}
}
}

/**
* Checks an array for a (partial) match with $string.
*
Expand Down
70 changes: 70 additions & 0 deletions tests/integration/AssetsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,20 @@

use Plausible\Analytics\Tests\TestCase;
use Plausible\Analytics\WP\Assets;
use Plausible\Analytics\WP\Integrations\FormSubmit;

class AssetsTest extends TestCase {
/**
* These tests act as a visitor that's tracked (logged out), whatever user a previous test left logged in.
*
* @return void
*/
protected function set_up() {
parent::set_up();

wp_set_current_user( 0 );
}

/**
* @see Assets::maybe_enqueue_cloaked_affiliate_links_assets()
* @return void
Expand Down Expand Up @@ -175,4 +187,62 @@ public function testEnqueueSearchQueriesScript() {
remove_filter( 'plausible_analytics_is_search', '__return_true' );
}
}

/**
* For a user role that isn't tracked (administrators by default), the tracker is registered but not enqueued, and
* neither is its plausible.init() call. Scripts that depend on it mustn't pull it onto the page anyway: the tracker
* then leaves window.plausible an object, so their calls throw "window.plausible is not a function".
*
* @see Assets::maybe_enqueue_cloaked_affiliate_links_assets()
* @see FormSubmit::add_js()
* @return void
* @throws \Exception
*/
public function testIntegrationsDontLoadTheTrackerForUntrackedRoles() {
$user_id = wp_insert_user( [ 'user_login' => 'pa_untracked_' . wp_generate_password( 6, false ), 'user_pass' => wp_generate_password(), 'role' => 'administrator' ] );

wp_set_current_user( $user_id );
add_filter( 'plausible_analytics_settings', [ $this, 'enableCloakedAffiliateLinks' ] );
add_filter( 'plausible_analytics_settings', [ $this, 'enableFormCompletions' ] );

$assets = $this->getMockBuilder( Assets::class )->disableOriginalConstructor()->onlyMethods( [ 'get_js_url' ] )->getMock();
$assets->method( 'get_js_url' )->willReturn( 'https://plausible.test/js/plausible.js' );
$form_submit = $this->getMockBuilder( FormSubmit::class )->disableOriginalConstructor()->onlyMethods( [] )->getMock();

try {
$this->dequeueMainScript();

$assets->maybe_enqueue_main_script();

// Another plugin's script that depends on the tracker makes WordPress report the tracker as enqueued.
wp_enqueue_script( 'pa-test-other-plugin', 'https://example.org/other-plugin.js', [ 'plausible-analytics' ], null );

$form_submit->add_js();
$assets->maybe_enqueue_cloaked_affiliate_links_assets();

$this->assertTrue( wp_script_is( 'plausible-analytics', 'registered' ) );
$this->assertFalse( \Plausible\Analytics\WP\Helpers::main_script_is_initialized() );
$this->assertFalse( wp_script_is( 'plausible-form-submit-integration', 'enqueued' ) );
$this->assertFalse( wp_script_is( 'plausible-affiliate-links', 'enqueued' ) );

// Once administrators are tracked, everything is loaded.
add_filter( 'plausible_analytics_settings', [ $this, 'enableAdministratorTracking' ] );

$assets->maybe_enqueue_main_script();
$form_submit->add_js();
$assets->maybe_enqueue_cloaked_affiliate_links_assets();

$this->assertTrue( \Plausible\Analytics\WP\Helpers::main_script_is_initialized() );
$this->assertTrue( wp_script_is( 'plausible-form-submit-integration', 'enqueued' ) );
$this->assertTrue( wp_script_is( 'plausible-affiliate-links', 'enqueued' ) );
} finally {
remove_filter( 'plausible_analytics_settings', [ $this, 'enableCloakedAffiliateLinks' ] );
remove_filter( 'plausible_analytics_settings', [ $this, 'enableFormCompletions' ] );
remove_filter( 'plausible_analytics_settings', [ $this, 'enableAdministratorTracking' ] );
wp_dequeue_script( 'pa-test-other-plugin' );
$this->dequeueMainScript();
wp_set_current_user( 0 );
wp_delete_user( $user_id );
}
}
}
Loading
Loading