Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
88ff04a
Premium Analytics: Wire Simple bootstrap behind gate
kangzj Jul 7, 2026
babe677
Premium Analytics: Fix WPCOM mirror build dependencies
kangzj Jul 7, 2026
13f385e
Premium Analytics: Clean up mirror build metadata
kangzj Jul 7, 2026
87f69ec
Premium Analytics: Fix WPCOMSH dependency solve
kangzj Jul 7, 2026
3d835fb
Premium Analytics: Cover Simple loader
kangzj Jul 7, 2026
b5a9e2a
Premium Analytics: Refresh WPCOM lock references
kangzj Jul 7, 2026
551e0aa
Premium Analytics: Fix WPCOMSH root solve
kangzj Jul 7, 2026
f895e34
Premium Analytics: Use WPCOM namespace for dashboard routes
kangzj Jul 8, 2026
e58cc6f
Premium Analytics: Build WPCOM API paths on Simple
kangzj Jul 8, 2026
bee2c77
Premium Analytics: Consolidate Simple transport in the data layer
kangzj Jul 15, 2026
0417315
Premium Analytics: Pin jest to UTC
kangzj Jul 15, 2026
dfe0d0b
Premium Analytics: Drop composer versions overrides
kangzj Jul 15, 2026
880441f
Premium Analytics: Stub isSimpleSite in the top-posts widget test
kangzj Jul 15, 2026
7530186
Premium Analytics: Route report fetchers through the stats transport
kangzj Jul 15, 2026
64c372a
Premium Analytics: Skip dashboard route registration on Simple
kangzj Jul 15, 2026
0bb3028
Premium Analytics: Compose init entry points from atomic boot steps
kangzj Jul 15, 2026
067ebcc
Revert "Premium Analytics: Drop composer versions overrides"
kangzj Jul 15, 2026
f40506b
Premium Analytics: Inline single-call boot steps
kangzj Jul 15, 2026
cacb7cf
Premium Analytics: Refresh WPCOMSH blaze lock reference
kangzj Jul 15, 2026
26712c5
Premium Analytics: Fix Simple readiness redirect loop on report routes
kangzj Jul 16, 2026
8a2fd0e
Premium Analytics: Fix calendar-diff timezone bug instead of pinning …
kangzj Jul 16, 2026
8dba5f4
Premium Analytics: Harden the stats proxy transport per review
kangzj Jul 16, 2026
f4d6211
Docs: Explain composer versions pins for branchless CI builds
kangzj Jul 16, 2026
0b03755
Premium Analytics: Expose a single entry point for the WPCOM dashboar…
kangzj Jul 19, 2026
beca3cd
Premium Analytics: Simplify the boot sequence and route-registration API
kangzj Jul 19, 2026
123af62
Premium Analytics: Tighten comments, document the WPCOM call contract
kangzj Jul 19, 2026
af2a3f5
Premium Analytics: Simplify the WordPress.com Simple AGENTS.md section
kangzj Jul 19, 2026
574f446
Premium Analytics: Refresh composer locks after rebasing onto trunk
kangzj Jul 19, 2026
1f7267d
Revert docs/monorepo.md composer versions pins explanation
kangzj Jul 19, 2026
20a9979
Premium Analytics: Suppress Phan duplicate-statement false positive i…
kangzj Jul 19, 2026
719938d
Premium Analytics: Document why dashboard routes use wpcom/v2, not je…
kangzj Jul 19, 2026
e4b4654
Premium Analytics: Give the new-endpoint namespace guidance its own s…
kangzj Jul 19, 2026
3198202
Premium Analytics: Document the shared site-readiness helper requirement
kangzj Jul 19, 2026
f0a418e
Premium Analytics: Lazy-load the widget registry and route files
kangzj Jul 19, 2026
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

WordPress.com Simple: Add gated Premium Analytics bootstrap wiring.
10 changes: 9 additions & 1 deletion projects/packages/jetpack-mu-wpcom/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"automattic/jetpack-google-analytics": "@dev",
"automattic/jetpack-masterbar": "@dev",
"automattic/jetpack-podcast": "@dev",
"automattic/jetpack-premium-analytics": "@dev",
"automattic/jetpack-redirect": "@dev",
"automattic/jetpack-rtc": "@dev",
"automattic/jetpack-stats-admin": "@dev",
Expand Down Expand Up @@ -63,7 +64,14 @@
"type": "path",
"url": "../../packages/*",
"options": {
"monorepo": true
"monorepo": true,
"versions": {
"automattic/block-delimiter": "dev-trunk",
"automattic/jetpack-assets": "dev-trunk",
"automattic/jetpack-connection": "dev-trunk",
"automattic/jetpack-constants": "dev-trunk",
"automattic/jetpack-device-detection": "dev-trunk"
}
}
}
],
Expand Down
38 changes: 38 additions & 0 deletions projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@

namespace Automattic\Jetpack;

use Automattic\Jetpack\PremiumAnalytics\Analytics as Premium_Analytics;

define( 'WPCOM_ADMIN_BAR_UNIFICATION', true );
/**
* Jetpack_Mu_Wpcom main class.
Expand Down Expand Up @@ -64,6 +66,7 @@ public static function init() {
add_action( 'plugins_loaded', array( __CLASS__, 'load_verbum_comments' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'load_verbum_moderate' ) );
add_action( 'wp_loaded', array( __CLASS__, 'load_verbum_comments_admin' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'load_wpcom_simple_premium_analytics' ) );
add_action( 'admin_menu', array( __CLASS__, 'load_wpcom_simple_odyssey_stats' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'load_wpcom_random_redirect' ) );
add_action( 'plugins_loaded', array( __CLASS__, 'load_podcast' ) );
Expand Down Expand Up @@ -774,6 +777,41 @@ public static function load_wpcom_simple_odyssey_stats() {
require_once __DIR__ . '/features/wpcom-simple-odyssey-stats/wpcom-simple-odyssey-stats.php';
}

/**
* Whether Premium Analytics should be loaded on WordPress.com Simple.
*
* @return bool True when the Simple rollout gate is enabled.
*/
public static function should_load_wpcom_simple_premium_analytics() {
$blog_id = (int) get_wpcom_blog_id();
$enabled = $blog_id > 0 && wpcom_has_blog_sticker( 'jetpack-premium-analytics', $blog_id );

/**
* Filters whether Premium Analytics loads on WordPress.com Simple.
*
* @since $$next-version$$
*
* @param bool $enabled Whether the Simple rollout gate is enabled.
* @param int $blog_id WPCOM blog ID.
*/
return (bool) apply_filters( 'jetpack_premium_analytics_wpcom_simple_enabled', $enabled, $blog_id );
}

/**
* Load Premium Analytics on WordPress.com Simple sites behind the rollout gate.
*/
public static function load_wpcom_simple_premium_analytics() {
if ( ! self::should_load_wpcom_simple_premium_analytics() ) {
return;
}

Premium_Analytics::init_wpcom_simple(
array(
'menu_title' => 'Premium Analytics',
)
);
}

/**
* Load the Jetpack Custom CSS feature.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php
/**
* Tests for the WordPress.com Simple Premium Analytics bootstrap gate.
*
* @package automattic/jetpack-mu-wpcom
*/

use Automattic\Jetpack\Jetpack_Mu_Wpcom;
use PHPUnit\Framework\Attributes\PreserveGlobalState;
use PHPUnit\Framework\Attributes\RunInSeparateProcess;

/**
* Tests for Premium Analytics Simple loading.
*/
class Wpcom_Simple_Premium_Analytics_Test extends \WorDBless\BaseTestCase {

/**
* Tear down.
*/
public function tear_down() {
remove_all_filters( 'jetpack_premium_analytics_wpcom_simple_enabled' );
\Mockery::close();

parent::tear_down();
}

/**
* The Simple bootstrap gate is disabled by default.
*/
public function test_wpcom_simple_premium_analytics_gate_is_off_by_default() {
$this->assertFalse( Jetpack_Mu_Wpcom::should_load_wpcom_simple_premium_analytics() );
}

/**
* The Simple bootstrap gate can be enabled for a staged site.
*/
public function test_wpcom_simple_premium_analytics_gate_can_be_enabled_by_filter() {
add_filter( 'jetpack_premium_analytics_wpcom_simple_enabled', '__return_true' );

$this->assertTrue( Jetpack_Mu_Wpcom::should_load_wpcom_simple_premium_analytics() );
}

/**
* The Simple loader returns before bootstrapping Premium Analytics when gated off.
*/
public function test_wpcom_simple_premium_analytics_loader_returns_when_gate_is_disabled() {
Jetpack_Mu_Wpcom::load_wpcom_simple_premium_analytics();

$this->assertFalse( class_exists( \Automattic\Jetpack\PremiumAnalytics\Analytics::class, false ) );
}

/**
* The Simple loader boots Premium Analytics when the rollout gate is enabled.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
#[RunInSeparateProcess]
#[PreserveGlobalState( false )]
public function test_wpcom_simple_premium_analytics_loader_boots_premium_analytics_when_enabled() {
add_filter( 'jetpack_premium_analytics_wpcom_simple_enabled', '__return_true' );

\Mockery::mock( 'alias:' . \Automattic\Jetpack\PremiumAnalytics\Analytics::class )
->shouldReceive( 'init_wpcom_simple' )
->once()
->with(
array(
'menu_title' => 'Premium Analytics',
)
);

Jetpack_Mu_Wpcom::load_wpcom_simple_premium_analytics();

$this->addToAssertionCount( 1 );
}

/**
* The Simple bootstrap gate can be enabled by the rollout sticker.
*
* @runInSeparateProcess
* @preserveGlobalState disabled
*/
#[RunInSeparateProcess]
#[PreserveGlobalState( false )]
public function test_wpcom_simple_premium_analytics_gate_can_be_enabled_by_sticker() {
if ( ! defined( 'IS_WPCOM' ) ) {
define( 'IS_WPCOM', true );
}

eval( 'function has_blog_sticker( $sticker, $blog_id ) { return $sticker === "jetpack-premium-analytics" && $blog_id > 0; }' ); // phpcs:ignore Squiz.PHP.Eval.Discouraged,MediaWiki.Usage.ForbiddenFunctions.eval

$this->assertTrue( Jetpack_Mu_Wpcom::should_load_wpcom_simple_premium_analytics() );
}
}
56 changes: 56 additions & 0 deletions projects/packages/premium-analytics/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,62 @@ routes (`jetpack/v4/stats-app/*`) → `stats` / `jetpack-stats` / `subscribers`
prefixes; Woo `analytics/reports/*` → `proxy/v2/analytics/reports/*`. The dashboard UI lives in
`routes/` here, not in Calypso. Frontend helpers go under `packages/data/src/api/`.

## WordPress.com Simple

Simple has no local proxy, notices, sync, or dashboard support routes — WPCOM serves the dashboard
and reaches `public-api.wordpress.com` directly. `jetpack-mu-wpcom` boots the package via
`Analytics::init_wpcom_simple()`, behind the `jetpack-premium-analytics` blog sticker.

### Route guards must use the shared site-readiness helpers

Every route's `beforeLoad` that checks connection or sync state must call
`isPremiumAnalyticsSiteConnected()` / `isPremiumAnalyticsInitialSyncFinished()` from
`routes/site-readiness.ts` — never read `getScriptData()?.connection?.connectionStatus?.isRegistered`
or `getScriptData()?.premium_analytics?.initial_full_sync_finished` directly. Simple has no Jetpack
connection, so a direct read silently evaluates to "not connected" there.

That's more than one broken route: it's a redirect loop. `/connect` and `/syncing` already go
through the shared helpers and treat Simple as connected and synced, so if a route added later
skips the helpers, Simple hits that route, gets redirected to `/connect`, and `/connect` — seeing
Simple as already connected — immediately redirects back to `/`. From the user's side this looks
like "the page just bounces to the dashboard," with nothing in the console pointing at the cause.
This shipped once (Automattic/jetpack#50266): the `/reports/$report` route was left reading script
data directly when the other four routes were migrated to the shared helpers, so it fell out of
sync with `/connect`'s guard and the two routes bounced traffic between each other on Simple.

Adding a new route with a connection/sync guard: grep `routes/` for
`isPremiumAnalyticsSiteConnected` first and copy that shape — don't re-derive the check from
script data.

### Why the dashboard support routes moved from `jetpack/v4` to `wpcom/v2`

The dashboard support routes (widget modules, default layout, sections) used to live under
`jetpack/v4` — the self-hosted Jetpack plugin's own namespace. WPCOM's REST centralization doesn't
recognize or expose that namespace for Simple sites, which run no Jetpack plugin at all, so those
routes were unreachable from public-api. `wpcom/v2` is a namespace WPCOM's centralization already
treats as site-specific by default for plain function-callback routes: registering under it is
enough for WPCOM to rewrite and expose the route as `/wpcom/v2/sites/<blog_id>/...` through
public-api, with no separate dotcom-side registration and no `wpcom_rest_api_v2_load_plugin()`
class shim required. That's why the rename happened (Automattic/jetpack#50266), and it's why
`Dashboard_Support_Routes::register()` exists as a standalone entry point WPCOM can call.

### Choosing a REST namespace for new endpoints

**Any future Premium Analytics REST endpoint that needs to work on both connected Jetpack sites
and WPCOM Simple must register under `wpcom/v2`** (via `DASHBOARD_REST_NAMESPACE` in
`src/rest-namespace.php`), not `jetpack/v4` or a plugin-specific namespace — those only reach
connected sites. An endpoint that's intentionally connected-site-only (e.g. the local data proxy,
notices) can stay under `jetpack-premium-analytics/v1`, since Simple never calls it.

**WPCOM's public-api process calls `Dashboard_Support_Routes::register()` directly**
(`src/class-dashboard-support-routes.php`) to register the dashboard's REST support routes
(widget modules, default layout, sections) standalone. The WPCOM-side caller is
`wp-content/rest-api-plugins/jetpack-endpoints/premium-analytics-dashboard.php` in the `wpcom`
repo — it `require_once`s this exact file and calls `::register()` by name.

**Renaming, moving, or changing this method's signature requires a matching WPCOM-side update.**
See Automattic/jetpack#50266 for the PR that established this contract.

## Pitfalls

- Never put the blog ID in a proxy path — it's injected server-side.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: added

Premium Analytics: Add a gated WordPress.com Simple bootstrap proof of concept.
9 changes: 8 additions & 1 deletion projects/packages/premium-analytics/composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,14 @@
"type": "path",
"url": "../*",
"options": {
"monorepo": true
"monorepo": true,
"versions": {
"automattic/block-delimiter": "dev-trunk",
"automattic/jetpack-assets": "dev-trunk",
"automattic/jetpack-connection": "dev-trunk",
"automattic/jetpack-constants": "dev-trunk",
"automattic/jetpack-device-detection": "dev-trunk"
}
}
}
],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
"types": "src/index.ts",
"sideEffects": false,
"dependencies": {
"@automattic/jetpack-script-data": "workspace:*",
"@date-fns/tz": "1.4.1",
"@jetpack-premium-analytics/datetime": "workspace:*",
"@tanstack/react-query": "5.101.2",
Expand Down
Loading