Skip to content
Open
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
7 changes: 7 additions & 0 deletions readme.txt
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,13 @@ Please make sure you make a backup of your database before updating any version

= 2.6.2 =
* Added: TranslatePress "different domain per language" (Multiple Domains) compatibility. Each language domain can be mapped to its own Plausible Analytics dashboard, just like WPML.
* Added: WPML Multilingual & Multicurrency for WooCommerce (WooCommerce Multilingual) compatibility.
* Fixed: the view product goal is now created for the URL each language is served under, e.g. /es/producto*, instead of for the default language's URL only. This covers both the language prefix (/es/)
and a translated product base (WooCommerce Multilingual's Store URLs), so product pageviews are no longer missed in any language but the default one. Because a funnel step holds a single goal, the default language's path remains the first step of the purchase funnel, and every other language gets a goal of its own. This applies to WooCommerce as well as Easy Digital Downloads, on WPML as well as TranslatePress.
* Improved: the WooCommerce add to cart, remove from cart, start checkout, and complete purchase events now contain a currency property. Multicurrency plugins convert all amounts to the currency the
visitor is shopping in, which Plausible Analytics would otherwise sum up as if they were all in the same currency. The complete purchase event uses the currency the order was placed in.
* Improved: on multilingual sites, those same WooCommerce events now contain a language property, because translated products are separate posts, i.e., each language has its own product ID and product
name.
* Fixed: in "domain per language" mode, the proxy endpoint and the locally cached tracker script were loaded from the default domain, which could cause CORS errors on the other language domains.
* Fixed: Plugin Tokens for language domains whose key contains an underscore (e.g., TranslatePress' nl_NL) were stored under a stripped key, which meant they were never read back.
* Tested with WP 7.1.
Expand Down
2 changes: 2 additions & 0 deletions src/Admin/Provisioning.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ class Provisioning {
const CUSTOM_PROPERTIES = [
'cart_total',
'cart_total_items',
'currency',
'id',
'language',
'name',
'price',
'product_id',
Expand Down
197 changes: 170 additions & 27 deletions src/Admin/Provisioning/Integrations.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
namespace Plausible\Analytics\WP\Admin\Provisioning;

use Plausible\Analytics\WP\Admin\Provisioning;
use Plausible\Analytics\WP\Helpers;

class Integrations {
/**
Expand Down Expand Up @@ -49,57 +50,169 @@ private function init() {
}

/**
* @since 2.6.2 Added $post_type, to allow translating the Pageview goal's path.
*
* @param array $event_goals
* @param string $funnel_name
* @param string $post_type The integration's product post type, e.g. 'product'.
*
* @return void
* @codeCoverageIgnore We don't want to test the API.
*/
public function create_integration_funnel( $event_goals, $funnel_name ) {
$goals = [];
public function create_integration_funnel( $event_goals, $funnel_name, $post_type = '' ) {
$all_ids = $this->provisioning->normalize_option(
get_option( 'plausible_analytics_enhanced_measurements_goal_ids', [] )
);

foreach ( $event_goals as $event_key => $event_goal ) {
if ( $event_key === 'remove-from-cart' ) {
foreach ( $this->provisioning->get_clients() as $key => $client ) {
$all_ids = $this->provisioning->create_goals(
[ $this->provisioning->create_goal_request( $event_goal ) ],
$client,
$key,
$all_ids
);
foreach ( $this->provisioning->get_clients() as $key => $client ) {
$goals = [];
/**
* Goals which shouldn't (or can't) be part of the funnel.
*/
$extra_goals = [];

foreach ( $event_goals as $event_key => $event_goal ) {
if ( $event_key === 'remove-from-cart' ) {
$extra_goals[] = $this->provisioning->create_goal_request( $event_goal );

continue;
}

continue;
}
if ( $event_key === 'purchase' ) {
$currency = \Plausible\Analytics\WP\Integrations::is_edd_active() ? edd_get_currency() : get_woocommerce_currency();
$goals[] = $this->provisioning->create_goal_request( $event_goal, 'Revenue', $currency );

if ( $event_key === 'purchase' ) {
$currency = \Plausible\Analytics\WP\Integrations::is_edd_active() ? edd_get_currency() : get_woocommerce_currency();
$goals[] = $this->provisioning->create_goal_request( $event_goal, 'Revenue', $currency );
continue;
}

continue;
}
if ( $event_key === 'view-product' ) {
$paths = $this->get_pageview_goal_paths( $this->get_goal_path( $event_goal ), $key, $post_type );

/**
* A funnel step holds one goal, so the default language's path is the one that ends up in the
* funnel. The other languages get a goal of their own.
*/
$goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, array_shift( $paths ) );

foreach ( $paths as $path ) {
$extra_goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, $path );
}

if ( $event_key === 'view-product' ) {
$path = preg_replace( '/^.*?\//', '', $event_goal );
$goals[] = $this->provisioning->create_goal_request( $event_goal, 'Pageview', null, '/' . $path );
continue;
}

continue;
$goals[] = $this->provisioning->create_goal_request( $event_goal );
}

$goals[] = $this->provisioning->create_goal_request( $event_goal );
}
if ( ! empty( $extra_goals ) ) {
$all_ids = $this->provisioning->create_goals( $extra_goals, $client, $key, $all_ids );
}

foreach ( $this->provisioning->get_clients() as $key => $client ) {
$all_ids = $this->provisioning->create_funnel( $funnel_name, $goals, $client, $key, $all_ids );
}
}

/**
* Returns the path of a "Visit /some/path*" goal.
*
* @since 2.6.2
*
* @param string $event_goal
*
* @return string
*/
private function get_goal_path( $event_goal ) {
return '/' . preg_replace( '/^.*?\//', '', $event_goal );
}

/**
* Returns the Pageview goal paths for $path: one for each language that's served on $domain_key's domain.
*
* Multilingual plugins serve translated content under a language prefix (/es/product/...) and, when the post type's
* base slug is translated (e.g. WooCommerce Multilingual's Store URLs), under a translated base (/producto/...).
* A goal for the default language's path would never match those pageviews.
*
* The default language's path is always the first element.
*
* @since 2.6.2
*
* @param string $path E.g. /product*
* @param string $domain_key The Language Domain the goal is created for.
* @param string $post_type The post type $path's base slug belongs to.
*
* @return array
*
* @codeCoverageIgnore Because it depends on 3rd party plugins.
*/
private function get_pageview_goal_paths( $path, $domain_key, $post_type ) {
$languages = Helpers::get_active_languages();

if ( empty( $languages ) ) {
return [ $path ];
}

$default = Helpers::get_default_language();

if ( Helpers::is_language_per_domain_mode() ) {
// Each domain serves exactly one language, from its own root.
$languages = [ $domain_key === 'default' ? $default : $domain_key ];
} else {
$languages = array_merge( [ $default ], array_diff( $languages, [ $default ] ) );
}

$paths = [];

foreach ( array_filter( $languages ) as $language ) {
$paths[] = $this->localize_goal_path( $path, $language, $post_type );
}

$paths = array_values( array_unique( array_filter( $paths ) ) );

return ! empty( $paths ) ? $paths : [ $path ];
}

/**
* Rewrites $path to the URL the given language is served under, e.g. /product* > /es/producto*.
*
* @since 2.6.2
*
* @param string $path
* @param string $language_code
* @param string $post_type
*
* @return string
*
* @codeCoverageIgnore Because it depends on 3rd party plugins.
*/
private function localize_goal_path( $path, $language_code, $post_type ) {
$home_path = trim( (string) wp_parse_url( home_url( '/' ), PHP_URL_PATH ), '/' );
$relative = ltrim( $path, '/' );

// On multisite subdirectory installs the site's path precedes the language prefix.
if ( $home_path !== '' && strpos( $relative, "$home_path/" ) === 0 ) {
$relative = substr( $relative, strlen( $home_path ) + 1 );
} else {
$home_path = '';
}

$suffix = '';

if ( substr( $relative, -1 ) === '*' ) {
$suffix = '*';
$relative = substr( $relative, 0, -1 );
}

$slug = Helpers::translate_url_slug( trim( $relative, '/' ), $language_code, $post_type );
$parts = array_filter( [ $home_path, Helpers::get_language_url_prefix( $language_code ), $slug ] );

return '/' . implode( '/', $parts ) . $suffix;
}

/**
* Deletes the integration-specific goals using the stored goal IDs.
*
* @since 2.6.2 Also deletes the Pageview goals created for the other languages.
*
* @param object $integration The integration object containing event goals to be deleted.
*
* @return void
Expand All @@ -112,10 +225,11 @@ public function delete_integration_goals( $integration ) {
);

foreach ( $this->provisioning->get_clients() as $domain_key => $client ) {
$goals = $all_ids[ $domain_key ] ?? [];
$goals = $all_ids[ $domain_key ] ?? [];
$event_goals = $this->add_localized_event_goals( (array) $integration->event_goals, $domain_key, $integration->post_type ?? '' );

foreach ( $goals as $id => $name ) {
$key = $this->provisioning->array_search_contains( $name, $integration->event_goals );
$key = $this->provisioning->array_search_contains( $name, $event_goals );

if ( $key ) {
$client->delete_goal( $id );
Expand All @@ -132,4 +246,33 @@ public function delete_integration_goals( $integration ) {

update_option( 'plausible_analytics_enhanced_measurements_goal_ids', $all_ids );
}

/**
* Adds the view-product goal of every other language to $event_goals, so the goals created for those languages
* are recognized (and deleted) too.
*
* @since 2.6.2
*
* @param array $event_goals
* @param string $domain_key
* @param string $post_type
*
* @return array
*
* @codeCoverageIgnore Because it depends on 3rd party plugins.
*/
private function add_localized_event_goals( $event_goals, $domain_key, $post_type ) {
if ( empty( $event_goals['view-product'] ) ) {
return $event_goals;
}

$event_goal = $event_goals['view-product'];
$path = $this->get_goal_path( $event_goal );

foreach ( $this->get_pageview_goal_paths( $path, $domain_key, $post_type ) as $i => $localized_path ) {
$event_goals[ "view-product-$i" ] = str_replace( $path, $localized_path, $event_goal );
}

return $event_goals;
}
}
2 changes: 1 addition & 1 deletion src/Admin/Provisioning/Integrations/EDD.php
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ public function maybe_create_edd_funnel( $old_settings, $settings ) {

$edd = new Integrations\EDD( false );

$this->integrations->create_integration_funnel( $edd->event_goals, __( 'EDD Purchase Funnel', 'plausible-analytics' ) );
$this->integrations->create_integration_funnel( $edd->event_goals, __( 'EDD Purchase Funnel', 'plausible-analytics' ), $edd->post_type );
}

/**
Expand Down
2 changes: 1 addition & 1 deletion src/Admin/Provisioning/Integrations/WooCommerce.php
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ public function maybe_create_woocommerce_funnel( $old_settings, $settings ) {

$woocommerce = new Integrations\WooCommerce( false );

$this->integrations->create_integration_funnel( $woocommerce->event_goals, __( 'Woo Purchase Funnel', 'plausible-analytics' ) );
$this->integrations->create_integration_funnel( $woocommerce->event_goals, __( 'Woo Purchase Funnel', 'plausible-analytics' ), $woocommerce->post_type );
}

/**
Expand Down
50 changes: 50 additions & 0 deletions src/Admin/Upgrades.php
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ public function run() {
$this->upgrade_to_260();
}

if ( version_compare( $plausible_analytics_version, '2.6.2', '<' ) ) {
$this->upgrade_to_262();
}

// Add required upgrade routines for future versions here.
}

Expand Down Expand Up @@ -427,6 +431,52 @@ private function upgrade_to_260() {
update_option( 'plausible_analytics_version', '2.6.0' );
}

/**
* If a supported multilingual plugin (WPML, with or without WPML Multilingual & Multicurrency for WooCommerce, or
* TranslatePress) is active alongside WooCommerce or Easy Digital Downloads, and Ecommerce Revenue is enabled,
* recreate the goals and custom properties after updating the plugin.
*
* That way, existing installs get the Pageview goals for the languages they serve, e.g. /es/producto*, and the
* currency and language custom properties, without having to save their settings first.
*
* This runs on init (@see Upgrades::__construct()), i.e. after WPML registered its language API on plugin load and
* booted on plugins_loaded, so the languages the Pageview goal paths are built from are available.
*
* @since v2.6.2
*
* @return void
*
* @codeCoverageIgnore because all we'd be doing is testing the Plugins API.
*/
public function upgrade_to_262() {
$is_ecommerce = \Plausible\Analytics\WP\Integrations::is_wc_active() || \Plausible\Analytics\WP\Integrations::is_edd_active();

if ( Helpers::get_multilang_plugin() && $is_ecommerce &&
EnhancedMeasurements::is_enabled( EnhancedMeasurements::ECOMMERCE_REVENUE ) ) {
$provisioning = new Provisioning();
$integrations = new Integrations( $provisioning );
$settings = Helpers::get_settings();

/**
* These routines bail when no Plugin Token is entered yet, and each funnel is only created for the
* ecommerce plugin that's actually active. If no token is entered, the goals are created as soon as one
* is, so there's no need to run this routine again.
*
* @see Provisioning::maybe_provision_on_connect() Creates the goals and custom properties as soon as a
* Plugin Token is entered.
* @see Provisioning\Integrations\WooCommerce::init() Both funnels are (re)created on
* update_option_plausible_analytics_settings, i.e. when that token is saved.
* @see Provisioning\Integrations\EDD::init()
*/
$provisioning->maybe_create_custom_properties( [], $settings );

( new Provisioning\Integrations\WooCommerce( $integrations ) )->maybe_create_woocommerce_funnel( [], $settings );
( new Provisioning\Integrations\EDD( $integrations ) )->maybe_create_edd_funnel( [], $settings );
}

update_option( 'plausible_analytics_version', '2.6.2' );
}

/**
* Display a notice to CE users that haven't entered an API token yet.
*
Expand Down
Loading
Loading