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
771 changes: 606 additions & 165 deletions pnpm-lock.yaml

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ const StatsSection = () => {

const { counts, previousCounts, chartData } = processedData;

// The product's manage URL already points at whichever analytics UI this site
// runs; that decision belongs server-side, so it is not re-derived here.
const { manageUrl } = detail;
const { premiumAnalyticsEnabled = false } = getMyJetpackWindowInitialState( 'myJetpackFlags' );

/**
* Called when "See detailed stats" button is clicked.
*/
Expand All @@ -154,16 +159,22 @@ const StatsSection = () => {
product: slug,
} );

window.location.href = 'admin.php?page=stats&force_refresh=1';
}, [ recordEvent ] );
if ( ! manageUrl ) {
return;
}

// The Stats page caches its report, so the legacy link asks it to refresh.
// The dashboard fetches on load and has no such param.
window.location.href = premiumAnalyticsEnabled ? manageUrl : `${ manageUrl }&force_refresh=1`;
}, [ recordEvent, manageUrl, premiumAnalyticsEnabled ] );

const shouldShowSecondaryButton = useCallback(
() => !! ( status === PRODUCT_STATUSES.CAN_UPGRADE ),
[ status ]
() => !! ( status === PRODUCT_STATUSES.CAN_UPGRADE && manageUrl ),
[ status, manageUrl ]
);

const viewStatsButton = {
href: 'admin.php?page=stats',
href: manageUrl,
label: __( 'View detailed stats', 'jetpack-my-jetpack' ),
onClick: onDetailedStatsClick,
shouldShowButton: shouldShowSecondaryButton,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Stats: point the product card at the Premium Analytics dashboard when it replaces the Stats page.
3 changes: 3 additions & 0 deletions projects/packages/my-jetpack/src/class-initializer.php
Original file line number Diff line number Diff line change
Expand Up @@ -528,6 +528,9 @@ public static function get_my_jetpack_flags() {
$flags = array(
'videoPressStats' => Jetpack_Constants::is_true( 'JETPACK_MY_JETPACK_VIDEOPRESS_STATS_ENABLED' ),
'showFullJetpackStatsCard' => class_exists( 'Jetpack' ),
// Only says which destination `manage_url` is: the legacy Stats page
// caches its report and wants a `force_refresh` hint the dashboard does not.
'premiumAnalyticsEnabled' => Products\Stats::is_premium_analytics_enabled(),
);

return $flags;
Expand Down
38 changes: 38 additions & 0 deletions projects/packages/my-jetpack/src/products/class-stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -297,12 +297,46 @@ public static function has_trial_support() {
return true;
}

/**
* Mirrors `Analytics::MENU_PAGE_SLUG`, spelled out because My Jetpack does not
* depend on the premium-analytics package.
*
* @since $$next-version$$
*/
const PREMIUM_ANALYTICS_PAGE_SLUG = 'jetpack-premium-analytics-wp-admin';

/**
* Whether the Premium Analytics dashboard is the site's analytics UI.
*
* Guarded like the other `class_exists( 'Jetpack' )` checks here: My Jetpack
* also ships in plugins without the Jetpack plugin. Public so the UI flags
* report the same answer the URLs are built from.
*
* @since $$next-version$$
*
* @return bool
*/
public static function is_premium_analytics_enabled() {
return class_exists( 'Jetpack' )
&& method_exists( 'Jetpack', 'is_premium_analytics_enabled' )
&& \Jetpack::is_premium_analytics_enabled();
}

/**
* Get the WordPress.com URL for purchasing Jetpack Stats for the current site.
*
* Null once Premium Analytics is the analytics UI: the tier purchase
* screen was a Calypso route inside the Odyssey bundle, so it left with that
* dashboard. Null is also the base-class default, which falls the action
* button back to the existing `#/add-stats` interstitial.
*
* @return ?string
*/
public static function get_purchase_url() {
if ( self::is_premium_analytics_enabled() ) {
return null;
}

$status = static::get_status();
if ( $status === Products::STATUS_NEEDS_FIRST_SITE_CONNECTION ) {
return null;
Expand All @@ -323,6 +357,10 @@ public static function get_purchase_url() {
* @return ?string
*/
public static function get_manage_url() {
if ( self::is_premium_analytics_enabled() ) {
return admin_url( 'admin.php?page=' . self::PREMIUM_ANALYTICS_PAGE_SLUG );
}

return admin_url( 'admin.php?page=stats' );
}

Expand Down
126 changes: 126 additions & 0 deletions projects/packages/my-jetpack/tests/php/Stats_Product_Test.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
<?php

namespace Automattic\Jetpack\My_Jetpack;

use Automattic\Jetpack\Connection\Tokens;
use Automattic\Jetpack\My_Jetpack\Products\Stats;
use Jetpack_Options;
use PHPUnit\Framework\TestCase;
use WorDBless\Options as WorDBless_Options;
use WorDBless\Users as WorDBless_Users;

/**
* Unit tests for the Stats product's destination URLs.
*
* The Premium Analytics dashboard replaces the Stats page, so both URLs this
* product hands the UI have to follow it. The flag lives on the Jetpack plugin,
* mocked here through ./assets/jetpack-mock-plugin.txt.
*
* @package automattic/my-jetpack
* @see \Automattic\Jetpack\My_Jetpack\Products\Stats
*/
class Stats_Product_Test extends TestCase {

/**
* The current user id.
*
* @var int
*/
private static $user_id;

/**
* Setting up the test.
*/
public function setUp(): void {
parent::setUp();
$this->install_mock_plugins();
wp_cache_delete( 'plugins', 'plugins' );
activate_plugins( 'jetpack/jetpack.php' );

// Mock site connection, so the purchase URL is not short-circuited by the
// needs-first-site-connection status.
( new Tokens() )->update_blog_token( 'test.test.1' );
Jetpack_Options::update_option( 'id', 123 );

self::$user_id = wp_insert_user(
array(
'user_login' => 'test_admin',
'user_pass' => '123',
'role' => 'administrator',
)
);
wp_set_current_user( self::$user_id );
}

/**
* Installs the mock plugin present in the test assets folder as the Jetpack plugin.
*
* @return void
*/
public function install_mock_plugins() {
if ( ! file_exists( WP_PLUGIN_DIR . '/jetpack' ) ) {
mkdir( WP_PLUGIN_DIR . '/jetpack', 0777, true );
}
copy( __DIR__ . '/assets/jetpack-mock-plugin.txt', WP_PLUGIN_DIR . '/jetpack/jetpack.php' );
}

/**
* Returning the environment into its initial state.
*/
public function tearDown(): void {
// @phan-suppress-next-line PhanUndeclaredStaticProperty -- It's declared on the mock from ./assets/jetpack-mock-plugin.txt
\Jetpack::$mock_premium_analytics_enabled = false;

WorDBless_Options::init()->clear_options();
WorDBless_Users::init()->clear_all_users();

parent::tearDown();
}

/**
* Put the mocked Jetpack plugin's Premium Analytics flag into a known state.
*
* @param bool $enabled Whether Premium Analytics replaces the Stats page.
*/
private function set_premium_analytics_enabled( $enabled ) {
// @phan-suppress-next-line PhanUndeclaredStaticProperty -- It's declared on the mock from ./assets/jetpack-mock-plugin.txt
\Jetpack::$mock_premium_analytics_enabled = $enabled;
}

public function test_manage_url_points_at_the_stats_page_by_default() {
$this->set_premium_analytics_enabled( false );

$this->assertSame( admin_url( 'admin.php?page=stats' ), Stats::get_manage_url() );
}

public function test_manage_url_follows_premium_analytics_when_enabled() {
$this->set_premium_analytics_enabled( true );

$manage_url = Stats::get_manage_url();

$this->assertSame( admin_url( 'admin.php?page=' . Stats::PREMIUM_ANALYTICS_PAGE_SLUG ), $manage_url );
$this->assertStringNotContainsString( 'page=stats', (string) $manage_url );
}

public function test_purchase_url_points_at_the_stats_purchase_screen_by_default() {
$this->set_premium_analytics_enabled( false );

$purchase_url = Stats::get_purchase_url();

$this->assertIsString( $purchase_url );
$this->assertStringContainsString( 'page=stats', $purchase_url );
$this->assertStringContainsString( '#!/stats/purchase/123', $purchase_url );
}

/**
* The tier purchase screen was a Calypso route inside the Odyssey bundle, so
* it left with the Stats dashboard. Returning null — the base class default —
* makes the action button fall back to the `#/add-stats` interstitial instead
* of linking to a route that no longer resolves.
*/
public function test_purchase_url_is_null_when_premium_analytics_is_enabled() {
$this->set_premium_analytics_enabled( true );

$this->assertNull( Stats::get_purchase_url() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,15 @@ class Jetpack {

static $return_false = false; // Force return false in action methods.

// Whether the Premium Analytics dashboard replaces the Stats page. Named apart from the real
// plugin's own private static of the same purpose, so tests setting it can't be confused for
// reaching into that one.
static $mock_premium_analytics_enabled = false;

public static function is_premium_analytics_enabled() {
return self::$mock_premium_analytics_enabled;
}

public static function is_module_active( $module_name ) {
return in_array( $module_name, self::$active_modules );
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: minor
Type: added

Publish the dashboard page slug, capability, and site timezone in Jetpack script data, so other Jetpack surfaces can link to the dashboard.
76 changes: 74 additions & 2 deletions projects/packages/premium-analytics/src/class-analytics.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,72 @@ private static function boot_shared_services() {
// CSV report export pipeline (WOOA7S-1581): hooks rest_api_init, so it must
// register on all requests. Self-gates on WooCommerce + Jetpack connection.
Export::configure();

self::register_script_data();
}

/**
* Announce to Jetpack's other surfaces that this dashboard is the site's
* analytics UI, so they link here instead of the Stats page. Publishing it
* from the package that owns the dashboard means the key exists exactly
* where the dashboard does. Registered from both init paths, so Simple gets
* it too.
*
* @return void
*/
private static function register_script_data() {
add_filter( 'jetpack_admin_js_script_data', array( static::class, 'add_script_data' ) );
}

/**
* Runs on nearly every admin page load, so the payload stays to two strings,
* a bool, and one capability check.
*
* @param array $data The script data.
* @return array The script data with the analytics key added.
*/
public static function add_script_data( $data ) {
$data['analytics'] = array(
'enabled' => true,
'page_slug' => self::MENU_PAGE_SLUG,
'can_view' => current_user_can( Capabilities::VIEW_ANALYTICS ),
'timezone' => self::site_timezone(),
);

return $data;
}

/**
* Prefers `timezone_string` over `gmt_offset`, matching the dashboard's own
* `getSiteTimezone()`: analytics links point at past dates, so they cross
* daylight-saving boundaries routinely, and a fixed offset applied to the far
* side of a transition shifts the day.
*
* @return string An IANA timezone name, or a `+HH:MM` UTC offset.
*/
private static function site_timezone() {
$timezone_string = get_option( 'timezone_string' );

if ( is_string( $timezone_string ) && $timezone_string !== '' ) {
return $timezone_string;
}

return self::format_gmt_offset( (float) get_option( 'gmt_offset' ) );
}

/**
* Format a GMT offset in hours as `+HH:MM`.
*
* @param float $offset The offset in hours, e.g. 5.5 or -8.
* @return string The formatted offset.
*/
private static function format_gmt_offset( $offset ) {
$sign = $offset < 0 ? '-' : '+';
$absolute = abs( $offset );
$hours = (int) floor( $absolute );
$minutes = (int) round( ( $absolute - $hours ) * 60 );

return sprintf( '%s%02d:%02d', $sign, $hours, $minutes );
}

/**
Expand Down Expand Up @@ -325,13 +391,19 @@ private static function register_admin_page() {
add_action( 'jetpack-premium-analytics_init', array( static::class, 'ensure_script_data' ) );
}

/**
* The admin page slug the dashboard menu registers. Published in script data
* so no caller has to hard-code it.
*/
const MENU_PAGE_SLUG = 'jetpack-premium-analytics-wp-admin';

/**
* Admin page slugs that render the Premium Analytics dashboard.
*
* Mirrors the slugs the wp-build interceptor renders (full-page and the
* wp-admin integrated variant).
*/
const DASHBOARD_PAGE_SLUGS = array( 'jetpack-premium-analytics', 'jetpack-premium-analytics-wp-admin' );
const DASHBOARD_PAGE_SLUGS = array( 'jetpack-premium-analytics', self::MENU_PAGE_SLUG );

/**
* Whether the current request is rendering a Premium Analytics dashboard page.
Expand Down Expand Up @@ -388,7 +460,7 @@ public static function register_admin_menu() {
esc_html( $menu_title ),
esc_html( $menu_title ),
Capabilities::VIEW_ANALYTICS,
'jetpack-premium-analytics-wp-admin',
self::MENU_PAGE_SLUG,
$render_callback,
'dashicons-chart-bar',
2
Expand Down
Loading
Loading