From 88ff04a16e8926622fe76f77adce4645828b1612 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Tue, 7 Jul 2026 20:30:50 +1200 Subject: [PATCH 01/34] Premium Analytics: Wire Simple bootstrap behind gate --- .../wooa7s-1636-premium-analytics-simple | 4 + .../packages/jetpack-mu-wpcom/composer.json | 1 + .../src/class-jetpack-mu-wpcom.php | 38 +++++++ .../Wpcom_Simple_Premium_Analytics_Test.php | 59 ++++++++++ .../changelog/wooa7s-1636-wpcom-simple-poc | 4 + .../__tests__/wpcom-simple-api-fetch.test.ts | 93 ++++++++++++++++ .../packages/init/src/index.ts | 5 + .../init/src/wpcom-simple-api-fetch.ts | 103 ++++++++++++++++++ .../routes/connect/route.tsx | 9 +- .../routes/dashboard/route.ts | 12 +- .../routes/post-detail/route.ts | 12 +- .../routes/site-readiness.ts | 24 ++++ .../routes/syncing/route.tsx | 15 ++- .../premium-analytics/src/class-analytics.php | 63 ++++++++--- .../tests/js/site-readiness.test.ts | 65 +++++++++++ .../tests/php/Analytics_Test.php | 42 +++++++ 16 files changed, 511 insertions(+), 38 deletions(-) create mode 100644 projects/packages/jetpack-mu-wpcom/changelog/wooa7s-1636-premium-analytics-simple create mode 100644 projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1636-wpcom-simple-poc create mode 100644 projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts create mode 100644 projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts create mode 100644 projects/packages/premium-analytics/routes/site-readiness.ts create mode 100644 projects/packages/premium-analytics/tests/js/site-readiness.test.ts diff --git a/projects/packages/jetpack-mu-wpcom/changelog/wooa7s-1636-premium-analytics-simple b/projects/packages/jetpack-mu-wpcom/changelog/wooa7s-1636-premium-analytics-simple new file mode 100644 index 000000000000..3f26feca0099 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/changelog/wooa7s-1636-premium-analytics-simple @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +WordPress.com Simple: Add gated Premium Analytics bootstrap wiring. diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json index 5d232968c523..0ffd78c77ed2 100644 --- a/projects/packages/jetpack-mu-wpcom/composer.json +++ b/projects/packages/jetpack-mu-wpcom/composer.json @@ -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", diff --git a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php index 1bd40d0325a9..a71d13ee3eab 100644 --- a/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php +++ b/projects/packages/jetpack-mu-wpcom/src/class-jetpack-mu-wpcom.php @@ -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. @@ -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' ) ); @@ -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. */ diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php new file mode 100644 index 000000000000..131694906313 --- /dev/null +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php @@ -0,0 +1,59 @@ +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 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() ); + } +} diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1636-wpcom-simple-poc b/projects/packages/premium-analytics/changelog/wooa7s-1636-wpcom-simple-poc new file mode 100644 index 000000000000..4c02d430dd17 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1636-wpcom-simple-poc @@ -0,0 +1,4 @@ +Significance: patch +Type: added + +Premium Analytics: Add a gated WordPress.com Simple bootstrap proof of concept. diff --git a/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts b/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts new file mode 100644 index 000000000000..d7dc90838089 --- /dev/null +++ b/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts @@ -0,0 +1,93 @@ +/** + * Internal dependencies + */ +import { getWpcomSimpleApiFetchOptions } from '../wpcom-simple-api-fetch'; + +/** + * Set minimal script data for WPCOM Simple apiFetch tests. + * + * @param blogId - WPCOM blog ID. + */ +function setScriptData( blogId = 12345 ) { + Object.defineProperty( window, 'JetpackScriptData', { + configurable: true, + value: { + site: { + host: 'wpcom', + wpcom: { + blog_id: blogId, + }, + }, + }, + } ); +} + +afterEach( () => { + delete window.JetpackScriptData; +} ); + +describe( 'getWpcomSimpleApiFetchOptions', () => { + it( 'rewrites v2 proxy requests to wpcom v2 paths', () => { + expect( + getWpcomSimpleApiFetchOptions( { + path: '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/sessions/by-date?interval=day', + method: 'GET', + } ) + ).toEqual( { + path: '/wpcom/v2/analytics/reports/sessions/by-date?interval=day', + method: 'GET', + } ); + } ); + + it( 'rewrites v1 proxy requests to rest versioned paths', () => { + expect( + getWpcomSimpleApiFetchOptions( { + path: '/jetpack-premium-analytics/v1/proxy/v1.1/stats/visits?period=day', + } ) + ).toEqual( { + path: '/rest/v1.1/stats/visits?period=day', + } ); + } ); + + it( 'rewrites notices to the WPCOM Stats notices endpoint', () => { + expect( + getWpcomSimpleApiFetchOptions( { + path: '/jetpack-premium-analytics/v1/notices?force_refresh=true', + } ) + ).toEqual( { + path: '/wpcom/v2/jetpack-stats-dashboard/notices?force_refresh=true', + } ); + } ); + + it( 'leaves non Premium Analytics paths alone', () => { + const options = { path: '/wp/v2/settings' }; + + expect( getWpcomSimpleApiFetchOptions( options ) ).toBe( options ); + } ); + + it( 'keeps upgrades global and adds the current site query', () => { + setScriptData( 67890 ); + + expect( + getWpcomSimpleApiFetchOptions( { + path: '/jetpack-premium-analytics/v1/proxy/v1.2/upgrades?type=transferred', + } ) + ).toEqual( { + path: '/rest/v1.2/upgrades?type=transferred&site=67890', + global: true, + } ); + } ); + + it( 'does not overwrite an explicit upgrades site query', () => { + setScriptData( 67890 ); + + expect( + getWpcomSimpleApiFetchOptions( { + path: '/jetpack-premium-analytics/v1/proxy/v1.2/upgrades?site=41', + } ) + ).toEqual( { + path: '/rest/v1.2/upgrades?site=41', + global: true, + } ); + } ); +} ); diff --git a/projects/packages/premium-analytics/packages/init/src/index.ts b/projects/packages/premium-analytics/packages/init/src/index.ts index 4dd80aa0b7f7..0f816a147210 100644 --- a/projects/packages/premium-analytics/packages/init/src/index.ts +++ b/projects/packages/premium-analytics/packages/init/src/index.ts @@ -6,6 +6,10 @@ import apiFetch from '@wordpress/api-fetch'; import { store as bootStore } from '@wordpress/boot'; import { dispatch } from '@wordpress/data'; import { chartBar } from '@wordpress/icons'; +/** + * Internal dependencies + */ +import { setupWpcomSimpleApiFetch } from './wpcom-simple-api-fetch'; // apiFetch middleware registers onto a shared, process-wide chain. Guard so // repeated init() calls (re-mount, HMR, a future second boot) don't stack @@ -41,6 +45,7 @@ function setupApiFetch(): void { */ export async function init(): Promise< void > { setupApiFetch(); + setupWpcomSimpleApiFetch(); dispatch( bootStore ).updateMenuItem( 'dashboard', { icon: chartBar, diff --git a/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts b/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts new file mode 100644 index 000000000000..43ce78b59d14 --- /dev/null +++ b/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts @@ -0,0 +1,103 @@ +/** + * External dependencies + */ +import { getScriptData, isSimpleSite } from '@automattic/jetpack-script-data'; +import apiFetch from '@wordpress/api-fetch'; + +type ApiFetchOptions = Parameters< typeof apiFetch >[ 0 ] & { + global?: boolean; +}; + +const localProxyPrefix = '/jetpack-premium-analytics/v1/proxy/'; +const localNoticesPath = '/jetpack-premium-analytics/v1/notices'; +const wpcomNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; + +let wpcomSimpleMiddlewareConfigured = false; + +/** + * Append the current site query required by global WPCOM endpoints. + * + * @param path - API path to update. + * @return API path with the current site query when available. + */ +function appendSiteQuery( path: string ): string { + if ( /([?&])site=/.test( path ) ) { + return path; + } + + const blogId = getScriptData()?.site?.wpcom?.blog_id; + if ( ! blogId ) { + return path; + } + + return `${ path }${ path.includes( '?' ) ? '&' : '?' }site=${ encodeURIComponent( blogId ) }`; +} + +/** + * Resolve the public-api namespace for a proxied request version. + * + * @param version - API version from the local proxy path. + * @return WPCOM public-api namespace path. + */ +function getWpcomApiBasePath( version: string ): string { + return version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; +} + +/** + * Rewrite local Premium Analytics proxy options for WPCOM Simple. + * + * @param options - apiFetch options. + * @return Rewritten apiFetch options when the request targets Premium Analytics. + */ +export function getWpcomSimpleApiFetchOptions( options: ApiFetchOptions ): ApiFetchOptions { + const path = options.path; + if ( typeof path !== 'string' ) { + return options; + } + + if ( path === localNoticesPath || path.startsWith( `${ localNoticesPath }?` ) ) { + return { + ...options, + path: `${ wpcomNoticesPath }${ path.slice( localNoticesPath.length ) }`, + }; + } + + if ( ! path.startsWith( localProxyPrefix ) ) { + return options; + } + + const proxyPath = path.slice( localProxyPrefix.length ); + const match = proxyPath.match( /^v([^/]+)(\/.*)?$/ ); + if ( ! match ) { + return options; + } + + const version = match[ 1 ]; + const endpointWithQuery = match[ 2 ] ?? ''; + const wpcomPath = `${ getWpcomApiBasePath( version ) }${ endpointWithQuery }`; + + if ( endpointWithQuery === '/upgrades' || endpointWithQuery.startsWith( '/upgrades?' ) ) { + return { + ...options, + path: appendSiteQuery( wpcomPath ), + global: true, + }; + } + + return { + ...options, + path: wpcomPath, + }; +} + +/** + * Register the WPCOM Simple apiFetch rewrite middleware. + */ +export function setupWpcomSimpleApiFetch(): void { + if ( wpcomSimpleMiddlewareConfigured || ! isSimpleSite() ) { + return; + } + + apiFetch.use( ( options, next ) => next( getWpcomSimpleApiFetchOptions( options ) ) ); + wpcomSimpleMiddlewareConfigured = true; +} diff --git a/projects/packages/premium-analytics/routes/connect/route.tsx b/projects/packages/premium-analytics/routes/connect/route.tsx index cd64320c42ea..90cb0e6d1ea4 100644 --- a/projects/packages/premium-analytics/routes/connect/route.tsx +++ b/projects/packages/premium-analytics/routes/connect/route.tsx @@ -1,8 +1,11 @@ /** * External dependencies */ -import { getScriptData } from '@automattic/jetpack-script-data'; import { redirect } from '@wordpress/route'; +/** + * Internal dependencies + */ +import { isPremiumAnalyticsSiteConnected } from '../site-readiness'; /** * Route guard for /connect. @@ -10,9 +13,7 @@ import { redirect } from '@wordpress/route'; */ export const route = { beforeLoad: () => { - const connectionStatus = getScriptData()?.connection?.connectionStatus; - - if ( connectionStatus?.isRegistered ) { + if ( isPremiumAnalyticsSiteConnected() ) { throw redirect( { to: '/' } ); } }, diff --git a/projects/packages/premium-analytics/routes/dashboard/route.ts b/projects/packages/premium-analytics/routes/dashboard/route.ts index 9efe27b05893..b2aac8e3464c 100644 --- a/projects/packages/premium-analytics/routes/dashboard/route.ts +++ b/projects/packages/premium-analytics/routes/dashboard/route.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import { getScriptData } from '@automattic/jetpack-script-data'; import { ensureCoreSettingsReady, normalizeReportParams } from '@jetpack-premium-analytics/data'; import { store as coreStore } from '@wordpress/core-data'; import { dispatch, select } from '@wordpress/data'; @@ -10,6 +9,10 @@ import { redirect } from '@wordpress/route'; /** * Internal dependencies */ +import { + isPremiumAnalyticsInitialSyncFinished, + isPremiumAnalyticsSiteConnected, +} from '../site-readiness'; import { DASHBOARD_REST_NAMESPACE } from './hooks/constants'; type DashboardSearch = Record< string, string | undefined >; @@ -43,14 +46,11 @@ type DashboardSearch = Record< string, string | undefined >; */ export const route = { beforeLoad: async ( { search }: { search?: DashboardSearch } = {} ) => { - const connectionStatus = getScriptData()?.connection?.connectionStatus; - - if ( ! connectionStatus?.isRegistered ) { + if ( ! isPremiumAnalyticsSiteConnected() ) { throw redirect( { to: '/connect' } ); } - const syncFinished = getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0; - if ( ! syncFinished ) { + if ( ! isPremiumAnalyticsInitialSyncFinished() ) { throw redirect( { to: '/syncing' } ); } diff --git a/projects/packages/premium-analytics/routes/post-detail/route.ts b/projects/packages/premium-analytics/routes/post-detail/route.ts index 73e51e701b62..340a3b8752ed 100644 --- a/projects/packages/premium-analytics/routes/post-detail/route.ts +++ b/projects/packages/premium-analytics/routes/post-detail/route.ts @@ -1,7 +1,6 @@ /** * External dependencies */ -import { getScriptData } from '@automattic/jetpack-script-data'; import { ensureCoreSettingsReady, normalizeReportParams } from '@jetpack-premium-analytics/data'; import { store as coreStore } from '@wordpress/core-data'; import { dispatch, select } from '@wordpress/data'; @@ -11,6 +10,10 @@ import { redirect } from '@wordpress/route'; * Internal dependencies */ import { DASHBOARD_REST_NAMESPACE } from '../dashboard/hooks/constants'; +import { + isPremiumAnalyticsInitialSyncFinished, + isPremiumAnalyticsSiteConnected, +} from '../site-readiness'; import { resolveTabId } from './config'; type PostDetailParams = { postId?: string }; @@ -41,14 +44,11 @@ export const route = { params, search, }: { params?: PostDetailParams; search?: PostDetailSearch } = {} ) => { - const connectionStatus = getScriptData()?.connection?.connectionStatus; - - if ( ! connectionStatus?.isRegistered ) { + if ( ! isPremiumAnalyticsSiteConnected() ) { throw redirect( { to: '/connect' } ); } - const syncFinished = getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0; - if ( ! syncFinished ) { + if ( ! isPremiumAnalyticsInitialSyncFinished() ) { throw redirect( { to: '/syncing' } ); } diff --git a/projects/packages/premium-analytics/routes/site-readiness.ts b/projects/packages/premium-analytics/routes/site-readiness.ts new file mode 100644 index 000000000000..061894134e09 --- /dev/null +++ b/projects/packages/premium-analytics/routes/site-readiness.ts @@ -0,0 +1,24 @@ +/** + * External dependencies + */ +import { getScriptData, isSimpleSite } from '@automattic/jetpack-script-data'; + +/** + * Check whether Premium Analytics can treat the current site as connected. + * + * @return Whether the site is connected for Premium Analytics. + */ +export function isPremiumAnalyticsSiteConnected(): boolean { + return isSimpleSite() || !! getScriptData()?.connection?.connectionStatus?.isRegistered; +} + +/** + * Check whether Premium Analytics can treat the initial sync as finished. + * + * @return Whether initial sync is finished for Premium Analytics. + */ +export function isPremiumAnalyticsInitialSyncFinished(): boolean { + return ( + isSimpleSite() || ( getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0 ) > 0 + ); +} diff --git a/projects/packages/premium-analytics/routes/syncing/route.tsx b/projects/packages/premium-analytics/routes/syncing/route.tsx index 6d07b8f0e8b9..0c145b37e20c 100644 --- a/projects/packages/premium-analytics/routes/syncing/route.tsx +++ b/projects/packages/premium-analytics/routes/syncing/route.tsx @@ -1,8 +1,14 @@ /** * External dependencies */ -import { getScriptData } from '@automattic/jetpack-script-data'; import { redirect } from '@wordpress/route'; +/** + * Internal dependencies + */ +import { + isPremiumAnalyticsInitialSyncFinished, + isPremiumAnalyticsSiteConnected, +} from '../site-readiness'; /** * Route guard for /syncing. @@ -11,14 +17,11 @@ import { redirect } from '@wordpress/route'; */ export const route = { beforeLoad: () => { - const connectionStatus = getScriptData()?.connection?.connectionStatus; - - if ( ! connectionStatus?.isRegistered ) { + if ( ! isPremiumAnalyticsSiteConnected() ) { throw redirect( { to: '/connect' } ); } - const syncFinished = getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0; - if ( syncFinished > 0 ) { + if ( isPremiumAnalyticsInitialSyncFinished() ) { throw redirect( { to: '/' } ); } }, diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 9bd7f9bbc476..e6234ee05c1d 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -47,6 +47,33 @@ class Analytics { * @return void */ public static function init( $options = array() ) { + self::boot( $options, true ); + } + + /** + * Initialize the Analytics app on WordPress.com Simple. + * + * Simple serves the dashboard from WPCOM and uses WPCOM's wp-admin apiFetch bridge to reach + * public-api.wordpress.com directly, so it does not register the local proxy, notices, or sync + * bootstrap surfaces used by connected Jetpack sites. + * + * @param array $options Optional configuration options. + * Supported keys: + * - menu_title (string): Admin menu label. + * @return void + */ + public static function init_wpcom_simple( $options = array() ) { + self::boot( $options, false ); + } + + /** + * Shared package bootstrap. + * + * @param array $options Optional configuration options. + * @param bool $register_local_services Whether to register connected-site local services. + * @return void + */ + private static function boot( $options = array(), $register_local_services = true ) { if ( self::$initialized ) { return; } @@ -57,22 +84,26 @@ public static function init( $options = array() ) { self::$menu_title = $options['menu_title']; } - // Keep the shared connection available when another connection-owning plugin is deactivated. - Connection_Configuration::configure(); - - // Always on: sync runs in cron; REST routes + registry serve REST requests - // (is_admin() false). REST_REQUEST isn't defined this early, so they - // self-gate on their own rest_api_init / init hooks. - Sync_Status_Tracker::configure(); - - // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so - // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands. - Sync_Configuration::register(); - Api_Proxy_Controller::register(); - Notices_Controller::register(); - - // Emit front-end page views into the Jetpack Stats pipeline. - Jetpack_Stats_Tracker::configure(); + // Connected-site services self-gate on their own hooks; Simple reaches + // WPCOM APIs directly through the WPCOM wp-admin apiFetch bridge. + if ( $register_local_services ) { + // Keep the shared connection available when another connection-owning plugin is deactivated. + Connection_Configuration::configure(); + + // Always on: sync runs in cron; REST routes + registry serve REST requests + // (is_admin() false). REST_REQUEST isn't defined this early, so they + // self-gate on their own rest_api_init / init hooks. + Sync_Status_Tracker::configure(); + + // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so + // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands. + Sync_Configuration::register(); + Api_Proxy_Controller::register(); + Notices_Controller::register(); + + // Piggybacks on the Jetpack Stats module; checks Jetpack connection state. + Jetpack_Stats_Tracker::configure(); + } // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). WooCommerce_Analytics_Tracker::configure(); diff --git a/projects/packages/premium-analytics/tests/js/site-readiness.test.ts b/projects/packages/premium-analytics/tests/js/site-readiness.test.ts new file mode 100644 index 000000000000..21ff3c27f100 --- /dev/null +++ b/projects/packages/premium-analytics/tests/js/site-readiness.test.ts @@ -0,0 +1,65 @@ +/** + * Internal dependencies + */ +import { + isPremiumAnalyticsInitialSyncFinished, + isPremiumAnalyticsSiteConnected, +} from '../../routes/site-readiness'; + +/** + * Set Jetpack script data for route readiness tests. + * + * @param data - Script data to expose on the window. + */ +function setScriptData( data: Record< string, unknown > ) { + Object.defineProperty( window, 'JetpackScriptData', { + configurable: true, + value: data, + } ); +} + +afterEach( () => { + delete window.JetpackScriptData; +} ); + +describe( 'Premium Analytics site readiness', () => { + it( 'treats WordPress.com Simple sites as connected and synced', () => { + setScriptData( { + site: { host: 'wpcom' }, + } ); + + expect( isPremiumAnalyticsSiteConnected() ).toBe( true ); + expect( isPremiumAnalyticsInitialSyncFinished() ).toBe( true ); + } ); + + it( 'uses Jetpack connection and sync state for non-Simple sites', () => { + setScriptData( { + site: { host: 'unknown' }, + connection: { + connectionStatus: { + isRegistered: true, + }, + }, + premium_analytics: { + initial_full_sync_finished: 123, + }, + } ); + + expect( isPremiumAnalyticsSiteConnected() ).toBe( true ); + expect( isPremiumAnalyticsInitialSyncFinished() ).toBe( true ); + } ); + + it( 'does not mark unsynced non-Simple sites as ready', () => { + setScriptData( { + site: { host: 'unknown' }, + connection: { + connectionStatus: { + isRegistered: true, + }, + }, + } ); + + expect( isPremiumAnalyticsSiteConnected() ).toBe( true ); + expect( isPremiumAnalyticsInitialSyncFinished() ).toBe( false ); + } ); +} ); diff --git a/projects/packages/premium-analytics/tests/php/Analytics_Test.php b/projects/packages/premium-analytics/tests/php/Analytics_Test.php index d74b600642cd..5326885a0534 100644 --- a/projects/packages/premium-analytics/tests/php/Analytics_Test.php +++ b/projects/packages/premium-analytics/tests/php/Analytics_Test.php @@ -24,9 +24,29 @@ class Analytics_Test extends TestCase { protected function tearDown(): void { unset( $_GET['page'] ); unset( $GLOBALS['current_screen'] ); + remove_all_actions( 'jetpack_sync_processed_actions' ); + remove_all_actions( 'plugins_loaded' ); + remove_all_actions( 'rest_api_init' ); + remove_all_actions( 'admin_menu' ); + remove_all_actions( 'jetpack-premium-analytics_init' ); + remove_all_filters( 'jetpack_admin_js_script_data' ); + remove_all_filters( 'rest_post_dispatch' ); + remove_all_filters( 'jetpack_stats_transient_cleanup_prefixes' ); + $this->reset_analytics_init_state(); parent::tearDown(); } + /** + * Reset the one-shot init guard between tests. + */ + private function reset_analytics_init_state() { + $property = new \ReflectionProperty( Analytics::class, 'initialized' ); + if ( PHP_VERSION_ID < 80500 ) { + $property->setAccessible( true ); + } + $property->setValue( null, false ); + } + /** * Test that the Analytics class can be instantiated. */ @@ -34,6 +54,28 @@ public function test_class_exists() { $this->assertTrue( class_exists( Analytics::class ) ); } + /** + * Normal package bootstrap registers the local proxy cache cleanup hook. + */ + public function test_init_registers_local_proxy_by_default() { + $this->reset_analytics_init_state(); + + Analytics::init(); + + $this->assertNotFalse( has_filter( 'jetpack_stats_transient_cleanup_prefixes' ) ); + } + + /** + * WordPress.com Simple mode skips local proxy registration. + */ + public function test_init_skips_local_proxy_in_wpcom_simple_mode() { + $this->reset_analytics_init_state(); + + Analytics::init_wpcom_simple(); + + $this->assertFalse( has_filter( 'jetpack_stats_transient_cleanup_prefixes' ) ); + } + /** * The full-page dashboard slug in admin is recognized as a dashboard request. */ From babe677583b2c4e8ae4cde1223472aa8a8fd7c17 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Tue, 7 Jul 2026 21:18:00 +1200 Subject: [PATCH 02/34] Premium Analytics: Fix WPCOM mirror build dependencies --- .../packages/premium-analytics/composer.json | 15 +- ...r-wooa7s-1636-simple-premium-analytics-poc | 4 + .../plugins/mu-wpcom-plugin/composer.lock | 204 +++++++++++++++++- ...r-wooa7s-1636-simple-premium-analytics-poc | 4 + .../plugins/premium-analytics/composer.lock | 8 +- ...r-wooa7s-1636-simple-premium-analytics-poc | 4 + projects/plugins/wpcomsh/composer.lock | 133 +++++++++++- 7 files changed, 368 insertions(+), 4 deletions(-) create mode 100644 projects/plugins/mu-wpcom-plugin/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc create mode 100644 projects/plugins/premium-analytics/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc create mode 100644 projects/plugins/wpcomsh/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc diff --git a/projects/packages/premium-analytics/composer.json b/projects/packages/premium-analytics/composer.json index 020a7855c061..397be56b7040 100644 --- a/projects/packages/premium-analytics/composer.json +++ b/projects/packages/premium-analytics/composer.json @@ -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" + } } } ], @@ -64,6 +71,12 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" + }, + "dependencies": { + "build": [ + "packages/block-delimiter", + "packages/device-detection" + ] } } } diff --git a/projects/plugins/mu-wpcom-plugin/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc b/projects/plugins/mu-wpcom-plugin/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc new file mode 100644 index 000000000000..c09dd9fa441f --- /dev/null +++ b/projects/plugins/mu-wpcom-plugin/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update composer.lock files. diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index 59c2fbd874f0..bd3fccf4db20 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -639,6 +639,77 @@ "relative": true } }, + { + "name": "automattic/jetpack-config", + "version": "dev-trunk", + "dist": { + "type": "path", + "url": "../../packages/config", + "reference": "c57552fadeaa0ee131577eea652128b3686f62cc" + }, + "require": { + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-account-protection": "@dev", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-import": "@dev", + "automattic/jetpack-jitm": "@dev", + "automattic/jetpack-post-list": "@dev", + "automattic/jetpack-publicize": "@dev", + "automattic/jetpack-search": "@dev", + "automattic/jetpack-stats": "@dev", + "automattic/jetpack-stats-admin": "@dev", + "automattic/jetpack-sync": "@dev", + "automattic/jetpack-videopress": "@dev", + "automattic/jetpack-waf": "@dev", + "automattic/jetpack-yoast-promo": "@dev" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-config", + "textdomain": "jetpack-config", + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-config/compare/v${old}...v${new}" + }, + "branch-alias": { + "dev-trunk": "3.1.x-dev" + }, + "dependencies": { + "test-only": [ + "packages/account-protection", + "packages/connection", + "packages/import", + "packages/jitm", + "packages/post-list", + "packages/publicize", + "packages/search", + "packages/stats-admin", + "packages/stats", + "packages/sync", + "packages/videopress", + "packages/waf", + "packages/yoast-promo" + ] + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Jetpack configuration package that initializes other packages and configures Jetpack's functionality. Can be used as a base for all variants of Jetpack package usage.", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-connection", "version": "dev-trunk", @@ -1300,7 +1371,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "ca1d73cfbf0fac87c62e8b11d3528fe9f605b6ed" + "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" }, "require": { "automattic/block-delimiter": "@dev", @@ -1315,6 +1386,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", @@ -1649,6 +1721,84 @@ "relative": true } }, + { + "name": "automattic/jetpack-premium-analytics", + "version": "dev-trunk", + "dist": { + "type": "path", + "url": "../../packages/premium-analytics", + "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" + }, + "require": { + "automattic/jetpack-assets": "@dev", + "automattic/jetpack-config": "@dev", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-stats": "@dev", + "automattic/jetpack-sync": "@dev", + "automattic/jetpack-wp-build-polyfills": "@dev", + "automattic/woocommerce-analytics": "^0.16", + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-test-environment": "@dev", + "automattic/phpunit-select-config": "@dev", + "yoast/phpunit-polyfills": "^4.0.0" + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-premium-analytics", + "textdomain": "jetpack-premium-analytics", + "version-constants": { + "::PACKAGE_VERSION": "src/class-analytics.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-premium-analytics/compare/${old}...${new}" + }, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "dependencies": { + "build": [ + "packages/block-delimiter", + "packages/device-detection" + ] + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "phpunit-select-config phpunit.#.xml.dist --colors=always" + ], + "test-php": [ + "@composer phpunit" + ], + "test-php-coverage": [ + "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" + ], + "build-development": [ + "pnpm run build" + ], + "build-production": [ + "pnpm run build-production" + ], + "watch": [ + "Composer\\Config::disableProcessTimeout", + "pnpm run watch" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Premium Analytics dashboard for Jetpack sites.", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-redirect", "version": "dev-trunk", @@ -2282,6 +2432,58 @@ "relative": true } }, + { + "name": "automattic/woocommerce-analytics", + "version": "0.16.6", + "source": { + "type": "git", + "url": "https://github.com/Automattic/woocommerce-analytics.git", + "reference": "92b67c5f12b3f9b3dcb34e1b668743cfbe552666" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/woocommerce-analytics/zipball/92b67c5f12b3f9b3dcb34e1b668743cfbe552666", + "reference": "92b67c5f12b3f9b3dcb34e1b668743cfbe552666", + "shasum": "" + }, + "require": { + "automattic/block-delimiter": "^0.3", + "automattic/jetpack-assets": "^4.3", + "automattic/jetpack-connection": "^8.1", + "automattic/jetpack-constants": "^3.0", + "automattic/jetpack-device-detection": "^3.4", + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0", + "automattic/wordbless": "^0.5", + "woocommerce/woocommerce-sniffs": "1.0.0", + "yoast/phpunit-polyfills": "^4.0.0" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "changelogger": { + "changelog": "CHANGELOG.md" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Enhanced analytics for WooCommerce users.", + "support": { + "source": "https://github.com/Automattic/woocommerce-analytics/tree/v0.16.6" + }, + "time": "2026-06-17T08:21:41+00:00" + }, { "name": "league/uri", "version": "7.8.1", diff --git a/projects/plugins/premium-analytics/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc b/projects/plugins/premium-analytics/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc new file mode 100644 index 000000000000..c09dd9fa441f --- /dev/null +++ b/projects/plugins/premium-analytics/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update composer.lock files. diff --git a/projects/plugins/premium-analytics/composer.lock b/projects/plugins/premium-analytics/composer.lock index debd0596c528..79eaf9eee903 100644 --- a/projects/plugins/premium-analytics/composer.lock +++ b/projects/plugins/premium-analytics/composer.lock @@ -841,7 +841,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "c1cc2e268a640efec6587b9b21f013a6165e9b0f" + "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" }, "require": { "automattic/jetpack-assets": "@dev", @@ -872,6 +872,12 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" + }, + "dependencies": { + "build": [ + "packages/block-delimiter", + "packages/device-detection" + ] } }, "autoload": { diff --git a/projects/plugins/wpcomsh/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc b/projects/plugins/wpcomsh/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc new file mode 100644 index 000000000000..c09dd9fa441f --- /dev/null +++ b/projects/plugins/wpcomsh/changelog/jasper-wooa7s-1636-simple-premium-analytics-poc @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Update composer.lock files. diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index 7e3d5b0b1f63..a74994998003 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1487,7 +1487,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "ca1d73cfbf0fac87c62e8b11d3528fe9f605b6ed" + "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" }, "require": { "automattic/block-delimiter": "@dev", @@ -1502,6 +1502,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", @@ -1901,6 +1902,84 @@ "relative": true } }, + { + "name": "automattic/jetpack-premium-analytics", + "version": "dev-trunk", + "dist": { + "type": "path", + "url": "../../packages/premium-analytics", + "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" + }, + "require": { + "automattic/jetpack-assets": "@dev", + "automattic/jetpack-config": "@dev", + "automattic/jetpack-connection": "@dev", + "automattic/jetpack-stats": "@dev", + "automattic/jetpack-sync": "@dev", + "automattic/jetpack-wp-build-polyfills": "@dev", + "automattic/woocommerce-analytics": "^0.16", + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-test-environment": "@dev", + "automattic/phpunit-select-config": "@dev", + "yoast/phpunit-polyfills": "^4.0.0" + }, + "type": "jetpack-library", + "extra": { + "autotagger": true, + "mirror-repo": "Automattic/jetpack-premium-analytics", + "textdomain": "jetpack-premium-analytics", + "version-constants": { + "::PACKAGE_VERSION": "src/class-analytics.php" + }, + "changelogger": { + "link-template": "https://github.com/Automattic/jetpack-premium-analytics/compare/${old}...${new}" + }, + "branch-alias": { + "dev-trunk": "0.1.x-dev" + }, + "dependencies": { + "build": [ + "packages/block-delimiter", + "packages/device-detection" + ] + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "scripts": { + "phpunit": [ + "phpunit-select-config phpunit.#.xml.dist --colors=always" + ], + "test-php": [ + "@composer phpunit" + ], + "test-php-coverage": [ + "php -dpcov.directory=. ./vendor/bin/phpunit-select-config phpunit.#.xml.dist --coverage-php \"$COVERAGE_DIR/php.cov\"" + ], + "build-development": [ + "pnpm run build" + ], + "build-production": [ + "pnpm run build-production" + ], + "watch": [ + "Composer\\Config::disableProcessTimeout", + "pnpm run watch" + ] + }, + "license": [ + "GPL-2.0-or-later" + ], + "description": "Premium Analytics dashboard for Jetpack sites.", + "transport-options": { + "relative": true + } + }, { "name": "automattic/jetpack-redirect", "version": "dev-trunk", @@ -2609,6 +2688,58 @@ ], "time": "2026-03-27T12:47:04+00:00" }, + { + "name": "automattic/woocommerce-analytics", + "version": "0.16.6", + "source": { + "type": "git", + "url": "https://github.com/Automattic/woocommerce-analytics.git", + "reference": "92b67c5f12b3f9b3dcb34e1b668743cfbe552666" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Automattic/woocommerce-analytics/zipball/92b67c5f12b3f9b3dcb34e1b668743cfbe552666", + "reference": "92b67c5f12b3f9b3dcb34e1b668743cfbe552666", + "shasum": "" + }, + "require": { + "automattic/block-delimiter": "^0.3", + "automattic/jetpack-assets": "^4.3", + "automattic/jetpack-connection": "^8.1", + "automattic/jetpack-constants": "^3.0", + "automattic/jetpack-device-detection": "^3.4", + "php": ">=7.2" + }, + "require-dev": { + "automattic/jetpack-changelogger": "3.3.0", + "automattic/wordbless": "^0.5", + "woocommerce/woocommerce-sniffs": "1.0.0", + "yoast/phpunit-polyfills": "^4.0.0" + }, + "suggest": { + "automattic/jetpack-autoloader": "Allow for better interoperability with other plugins that use this package." + }, + "type": "jetpack-library", + "extra": { + "changelogger": { + "changelog": "CHANGELOG.md" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-2.0-or-later" + ], + "description": "Enhanced analytics for WooCommerce users.", + "support": { + "source": "https://github.com/Automattic/woocommerce-analytics/tree/v0.16.6" + }, + "time": "2026-06-17T08:21:41+00:00" + }, { "name": "league/uri", "version": "7.8.1", From 13f385e77e2543d1b9d51cccc7a931c271255a0c Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 10:07:52 +1200 Subject: [PATCH 03/34] Premium Analytics: Clean up mirror build metadata --- projects/packages/premium-analytics/composer.json | 6 ------ projects/plugins/mu-wpcom-plugin/composer.lock | 8 +------- projects/plugins/premium-analytics/composer.lock | 8 +------- projects/plugins/wpcomsh/composer.lock | 8 +------- 4 files changed, 3 insertions(+), 27 deletions(-) diff --git a/projects/packages/premium-analytics/composer.json b/projects/packages/premium-analytics/composer.json index 397be56b7040..e5dccf9c5dcf 100644 --- a/projects/packages/premium-analytics/composer.json +++ b/projects/packages/premium-analytics/composer.json @@ -71,12 +71,6 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" - }, - "dependencies": { - "build": [ - "packages/block-delimiter", - "packages/device-detection" - ] } } } diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index bd3fccf4db20..e8d8e8d48b09 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1727,7 +1727,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" + "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1757,12 +1757,6 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" - }, - "dependencies": { - "build": [ - "packages/block-delimiter", - "packages/device-detection" - ] } }, "autoload": { diff --git a/projects/plugins/premium-analytics/composer.lock b/projects/plugins/premium-analytics/composer.lock index 79eaf9eee903..ad94511f0030 100644 --- a/projects/plugins/premium-analytics/composer.lock +++ b/projects/plugins/premium-analytics/composer.lock @@ -841,7 +841,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" + "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" }, "require": { "automattic/jetpack-assets": "@dev", @@ -872,12 +872,6 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" - }, - "dependencies": { - "build": [ - "packages/block-delimiter", - "packages/device-detection" - ] } }, "autoload": { diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index a74994998003..617b9101267d 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1908,7 +1908,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "d81dd55f9d407692783082e8284dfcafb1c9c873" + "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1938,12 +1938,6 @@ }, "branch-alias": { "dev-trunk": "0.1.x-dev" - }, - "dependencies": { - "build": [ - "packages/block-delimiter", - "packages/device-detection" - ] } }, "autoload": { From 87f69ec8e8d002dfa3c6c998f59acb0850797f75 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 10:26:44 +1200 Subject: [PATCH 04/34] Premium Analytics: Fix WPCOMSH dependency solve --- projects/packages/jetpack-mu-wpcom/composer.json | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json index 0ffd78c77ed2..4b0372445253 100644 --- a/projects/packages/jetpack-mu-wpcom/composer.json +++ b/projects/packages/jetpack-mu-wpcom/composer.json @@ -64,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" + } } } ], From 3d835fb90da3bc278f9af58ed9ae6d4004fa95b4 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 10:36:18 +1200 Subject: [PATCH 05/34] Premium Analytics: Cover Simple loader --- .../Wpcom_Simple_Premium_Analytics_Test.php | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php b/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php index 131694906313..9b89c9ec0909 100644 --- a/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php +++ b/projects/packages/jetpack-mu-wpcom/tests/php/features/premium-analytics/Wpcom_Simple_Premium_Analytics_Test.php @@ -19,6 +19,7 @@ class Wpcom_Simple_Premium_Analytics_Test extends \WorDBless\BaseTestCase { */ public function tear_down() { remove_all_filters( 'jetpack_premium_analytics_wpcom_simple_enabled' ); + \Mockery::close(); parent::tear_down(); } @@ -39,6 +40,40 @@ public function test_wpcom_simple_premium_analytics_gate_can_be_enabled_by_filte $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. * From b5a9e2ab90049385854b74f8f6d2bc60b5756e7c Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 10:54:23 +1200 Subject: [PATCH 06/34] Premium Analytics: Refresh WPCOM lock references --- projects/plugins/mu-wpcom-plugin/composer.lock | 2 +- projects/plugins/wpcomsh/composer.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index e8d8e8d48b09..41e6b84f1bfe 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1371,7 +1371,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" + "reference": "f5a0802d45daac83c0bf9df90cf6f330527227cf" }, "require": { "automattic/block-delimiter": "@dev", diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index 617b9101267d..f3d515e41e4e 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1487,7 +1487,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" + "reference": "f5a0802d45daac83c0bf9df90cf6f330527227cf" }, "require": { "automattic/block-delimiter": "@dev", From 551e0aad959f57f4861cb456c9450e9591ffebfd Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 11:18:52 +1200 Subject: [PATCH 07/34] Premium Analytics: Fix WPCOMSH root solve --- projects/plugins/wpcomsh/composer.json | 9 ++- projects/plugins/wpcomsh/composer.lock | 78 +++++++++++++------------- 2 files changed, 47 insertions(+), 40 deletions(-) diff --git a/projects/plugins/wpcomsh/composer.json b/projects/plugins/wpcomsh/composer.json index b5113604c94b..4df53e7f4412 100644 --- a/projects/plugins/wpcomsh/composer.json +++ b/projects/plugins/wpcomsh/composer.json @@ -62,7 +62,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" + } } }, { diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index f3d515e41e4e..dc596b40ab29 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d0871f7a7395d1ee5864c7d40d2e35df", + "content-hash": "c9d4bc712b476604df193f64608b93ab", "packages": [ { "name": "automattic/block-delimiter", @@ -12,7 +12,7 @@ "dist": { "type": "path", "url": "../../packages/block-delimiter", - "reference": "e860d7feb6347341faad763d7e342cc6a5e8b449" + "reference": "bb5cd334fb6d00ed5d5c26f6f7cff9b3ae55ae99" }, "require": { "php": ">=7.2" @@ -123,7 +123,7 @@ "dist": { "type": "path", "url": "../../packages/a8c-mc-stats", - "reference": "48895090b425c307b50fdf77f2c770719e1d3cf4" + "reference": "003b710eb1eaf27ed970000b41a292c89fcfe5ba" }, "require": { "php": ">=7.2" @@ -176,7 +176,7 @@ "dist": { "type": "path", "url": "../../packages/admin-ui", - "reference": "96398da45af93491fdd90cea96331e624b42e39e" + "reference": "edc5a654cdffde980eafb17c2c1d98ca755957ad" }, "require": { "automattic/jetpack-redirect": "@dev", @@ -253,7 +253,7 @@ "dist": { "type": "path", "url": "../../packages/agents-manager", - "reference": "b4b58a385a26210a56620238aee1779e9373d195" + "reference": "7a2c9058e35e94a42562eee55d0528376601801b" }, "require": { "automattic/jetpack-assets": "@dev", @@ -328,7 +328,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "7214a03808294ae652164b2495dc4e8a09c54e89" + "reference": "5b5ad5da3b83d4a98780a175d17df9fe61e58350" }, "require": { "automattic/jetpack-constants": "@dev", @@ -401,7 +401,7 @@ "dist": { "type": "path", "url": "../../packages/autoloader", - "reference": "afee041b24440e62c6ddb502b3c6716044f8c3ed" + "reference": "e38ca7a8dc245675a896e6f1e126bdf6b5157ba6" }, "require": { "composer-plugin-api": "^2.2", @@ -469,7 +469,7 @@ "dist": { "type": "path", "url": "../../packages/blaze", - "reference": "66186f7bc5595357512833c76816e8496b6a6c0b" + "reference": "6f33631a30fe59d482659c8ce80da39e349a0e83" }, "require": { "automattic/jetpack-admin-ui": "@dev", @@ -545,7 +545,7 @@ "dist": { "type": "path", "url": "../../packages/blocks", - "reference": "21984233a8e0e6f3dd700bdaa35901f0f9c3162f" + "reference": "22e2fd3dc418c457cd5113b9cc4e040966ec2128" }, "require": { "automattic/jetpack-constants": "@dev", @@ -601,7 +601,7 @@ "dist": { "type": "path", "url": "../../packages/classic-theme-helper", - "reference": "de10e684e8c1cc0bdb66910f27aae22bdcfce75f" + "reference": "94619701fa1a58360a0a7e0afe159bc2dd7e5095" }, "require": { "automattic/jetpack-assets": "@dev", @@ -667,7 +667,7 @@ "dist": { "type": "path", "url": "../../packages/compat", - "reference": "cca3c26e1aa14e6cfe16ed55099dd1fc39362778" + "reference": "08939574a981fd4dc944070d7a3948decd9ff37a" }, "require": { "php": ">=7.2" @@ -701,7 +701,7 @@ "dist": { "type": "path", "url": "../../packages/composer-plugin", - "reference": "4c4b86f8b4a330c7033d2f49e33397d044a3d7a0" + "reference": "15799e1372ef4a4b2346310027df6bcef1c1ab87" }, "require": { "composer-plugin-api": "^2.2", @@ -761,7 +761,7 @@ "dist": { "type": "path", "url": "../../packages/config", - "reference": "c57552fadeaa0ee131577eea652128b3686f62cc" + "reference": "639ed7b8e15b027b74ede1651392cde0bf4928f4" }, "require": { "php": ">=7.2" @@ -832,7 +832,7 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "ce362d9d8bf97228d5267f8d4a9b05379ff98aa2" + "reference": "ac92fb106fb48e2bdfe0946da6e387b2f61585fb" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -920,7 +920,7 @@ "dist": { "type": "path", "url": "../../packages/constants", - "reference": "a2f68c778c6c47b3e9f7699379fe5efa8ffe2acc" + "reference": "7cc5b2f7d1f65f32d8ee5c1d11b6df716fcb6db1" }, "require": { "php": ">=7.2" @@ -974,7 +974,7 @@ "dist": { "type": "path", "url": "../../packages/device-detection", - "reference": "060e4e98b1fb7351b6635c5c8c16a0a2f365ce90" + "reference": "9ab4dcf177ba5b09afe97c13f6f538edf7ba5fd8" }, "require": { "php": ">=7.2" @@ -1095,7 +1095,7 @@ "dist": { "type": "path", "url": "../../packages/google-analytics", - "reference": "93748801f1647c1a443f8af82f79eac985303dfa" + "reference": "c03943f744703f6239ee5cc78fd5c29f2bc8de37" }, "require": { "automattic/jetpack-status": "@dev", @@ -1160,7 +1160,7 @@ "dist": { "type": "path", "url": "../../packages/image-cdn", - "reference": "9e71f33ab5611baafd105ddf7200e13be88fe827" + "reference": "ff73ec5136b5760ef6f0dc5dc7296a7a2fa70ae8" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1220,7 +1220,7 @@ "dist": { "type": "path", "url": "../../packages/ip", - "reference": "d63dccb293846fdd6ef54fb78848d043815de4bb" + "reference": "e487e4a8e010cf98cac25b18e858529de439dcb9" }, "require": { "php": ">=7.2" @@ -1278,7 +1278,7 @@ "dist": { "type": "path", "url": "../../packages/jitm", - "reference": "9060a71b2749d8026a07f334efc6e536114a7ced" + "reference": "c32c102004bd39788ef69c33756e132659506751" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -1353,7 +1353,7 @@ "dist": { "type": "path", "url": "../../packages/logo", - "reference": "9772ff10ed4f2e82abda216a2631b547d0a562e9" + "reference": "8e9b400315ba6fe6e556332c20310fb635f16185" }, "require": { "php": ">=7.2" @@ -1406,7 +1406,7 @@ "dist": { "type": "path", "url": "../../packages/masterbar", - "reference": "2c9c34203668df2e980fdf37d8f871bac19b11c9" + "reference": "09ed33eb6a354978d8e5e1d41bfa737d7a329e40" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1487,7 +1487,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "f5a0802d45daac83c0bf9df90cf6f330527227cf" + "reference": "ce82f8a3e5609f987876b4af12a00e797b7ef709" }, "require": { "automattic/block-delimiter": "@dev", @@ -1583,7 +1583,7 @@ "dist": { "type": "path", "url": "../../packages/password-checker", - "reference": "d9e7a7a8db710ebc8d064ae207775df8bd9e6662" + "reference": "8dd32a3ef5322b35a7caaaace68bd695605075cc" }, "require": { "php": ">=7.2" @@ -1638,7 +1638,7 @@ "dist": { "type": "path", "url": "../../packages/plans", - "reference": "d694876b1987293d1f9c0de3ff6ae37d8be95e85" + "reference": "a6e564eabb06e64767288117ae86db0432ec1099" }, "require": { "automattic/jetpack-connection": "@dev", @@ -1776,7 +1776,7 @@ "dist": { "type": "path", "url": "../../packages/post-list", - "reference": "1e835cbf7e005405652bee4c2f9460979de1bf92" + "reference": "1bf8d892fc7ff9671e0fa02d9e2101b707c6eee0" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1841,7 +1841,7 @@ "dist": { "type": "path", "url": "../../packages/post-media", - "reference": "e1c691c5b9ca6debdbf8351aaf6658189f1c0aa6" + "reference": "693f3207f6937211e1761f9d05b2864a74ff38ac" }, "require": { "automattic/block-delimiter": "@dev", @@ -1908,7 +1908,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" + "reference": "b3511e8ca26dd4052147f368ebf1b0aa9179a787" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1980,7 +1980,7 @@ "dist": { "type": "path", "url": "../../packages/redirect", - "reference": "84a8949d203512d279ad65280af8d2af01d4d799" + "reference": "654ba6e8dda4889250e4e671134aeb0bc8755481" }, "require": { "automattic/jetpack-status": "@dev", @@ -2035,7 +2035,7 @@ "dist": { "type": "path", "url": "../../packages/roles", - "reference": "a9e558a67db231ecf3194a02125a4b3f14a48b94" + "reference": "df8247a8970fdcc53b27916231a1ed5482f270f2" }, "require": { "php": ">=7.2" @@ -2089,7 +2089,7 @@ "dist": { "type": "path", "url": "../../packages/rtc", - "reference": "cf9257a7cb581975e66e8cee0e5dc04d3d4da7a1" + "reference": "62dcc023afd4f82da7ad6145e797122ab63091ad" }, "require": { "automattic/jetpack-assets": "@dev", @@ -2155,7 +2155,7 @@ "dist": { "type": "path", "url": "../../packages/stats", - "reference": "353dcc22a8de746e21bd2c6334d1ca7206fea704" + "reference": "e842f3502fce83a510a8115036d51edd9c5b1274" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2222,7 +2222,7 @@ "dist": { "type": "path", "url": "../../packages/stats-admin", - "reference": "69020479c6220930ee90793a30b40ecade9de84d" + "reference": "aa4d6effbd21f16943f4f5d4b80412c8711efc11" }, "require": { "automattic/jetpack-blaze": "@dev", @@ -2290,7 +2290,7 @@ "dist": { "type": "path", "url": "../../packages/status", - "reference": "30e50be852b4f6b22a2a47ed7ab3ef6daa4074b6" + "reference": "1d6ff66fe1a7869d8acef38cc32289aa3dbdd7c6" }, "require": { "automattic/jetpack-constants": "@dev", @@ -2354,7 +2354,7 @@ "dist": { "type": "path", "url": "../../packages/sync", - "reference": "0e6c312d73cf8b2bde57a5656e16e42c1b593217" + "reference": "6b066f38821fc99458a762fe0626c2b03c2ed819" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2426,7 +2426,7 @@ "dist": { "type": "path", "url": "../../packages/wp-abilities", - "reference": "97a1a9548ae11e0cd0f0514d81c0701474968828" + "reference": "7af51269db986f7bde09d44d66b497c0709e8b74" }, "require": { "php": ">=7.2" @@ -2484,7 +2484,7 @@ "dist": { "type": "path", "url": "../../packages/wp-build-polyfills", - "reference": "116ceba3c357f49d40854f3083f60faa372f5c25" + "reference": "552586a6a5288c977124576e399bd2ce3bd4df59" }, "require": { "php": ">=7.2" @@ -2550,7 +2550,7 @@ "dist": { "type": "path", "url": "../../packages/scheduled-updates", - "reference": "c9decd746c2bb13c832778fceafceac8ed06fec8" + "reference": "5cedcdf836af8bf383629f3d0eeb6df48f33ff7d" }, "require": { "automattic/jetpack-connection": "@dev", @@ -3928,7 +3928,7 @@ "dist": { "type": "path", "url": "../../packages/phpunit-select-config", - "reference": "f34d251d44e96b67a97272b464b69fc9ffb7de76" + "reference": "754e03c91b51aa261673505d13052e1fd754df21" }, "require": { "composer-runtime-api": "^2.2.2", From f895e348b41dc8bcef9f96e91ea2c02642a15bbe Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 15:52:46 +1200 Subject: [PATCH 08/34] Premium Analytics: Use WPCOM namespace for dashboard routes --- .../routes/dashboard/hooks/constants.ts | 4 +- .../routes/dashboard/route.ts | 2 + .../premium-analytics/src/class-analytics.php | 2 +- .../src/dashboard-layout.php | 6 +-- .../premium-analytics/src/rest-namespace.php | 13 +++++ .../premium-analytics/src/widget-modules.php | 8 +-- .../tests/js/dashboard-rest-namespace.test.ts | 10 ++++ .../tests/php/Dashboard_Layout_Test.php | 32 ++++++++++++ .../tests/php/Dashboard_Section_Test.php | 46 ++++++++--------- .../tests/php/Widget_Modules_Test.php | 50 +++++++++++++++++++ 10 files changed, 139 insertions(+), 34 deletions(-) create mode 100644 projects/packages/premium-analytics/src/rest-namespace.php create mode 100644 projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts create mode 100644 projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php diff --git a/projects/packages/premium-analytics/routes/dashboard/hooks/constants.ts b/projects/packages/premium-analytics/routes/dashboard/hooks/constants.ts index c9dd96ae43fe..04a819bb43b4 100644 --- a/projects/packages/premium-analytics/routes/dashboard/hooks/constants.ts +++ b/projects/packages/premium-analytics/routes/dashboard/hooks/constants.ts @@ -28,5 +28,5 @@ export const DASHBOARD_LAYOUT_KEY = 'dashboardLayout'; /** Preferences key holding the dashboard grid settings. */ export const DASHBOARD_GRID_SETTINGS_KEY = 'dashboardGridSettings'; -/** REST namespace that exposes the dashboard's default layout. */ -export const DASHBOARD_REST_NAMESPACE = 'jetpack/v4'; +/** REST namespace that exposes Premium Analytics dashboard endpoints. */ +export const DASHBOARD_REST_NAMESPACE = 'wpcom/v2'; diff --git a/projects/packages/premium-analytics/routes/dashboard/route.ts b/projects/packages/premium-analytics/routes/dashboard/route.ts index b2aac8e3464c..08bb543524af 100644 --- a/projects/packages/premium-analytics/routes/dashboard/route.ts +++ b/projects/packages/premium-analytics/routes/dashboard/route.ts @@ -37,6 +37,8 @@ type DashboardSearch = Record< string, string | undefined >; * so the stage's `getEntityRecords` read resolves and feeds the records to * `useWidgetTypes`. Premium Analytics serves the records from its own namespace * (see `src/widget-modules.php`), independent of core's `wp/v2` endpoint. + * The route is registered under `wpcom/v2` so WPCOM can expose it through the + * site-scoped public-api path for Simple sites. * Guarded for idempotency: beforeLoad re-runs on every navigation and preload. * * That registration is one-time bootstrap setup that could move to the page's diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index e6234ee05c1d..5febd82e8648 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -142,7 +142,7 @@ private static function boot( $options = array(), $register_local_services = tru // Load wp-build output (interceptor, modules, routes, page render). // Must stay above the is_admin() gate: build/widgets.php defines the // manifest the widget registry reads, and the registry serves REST - // requests (e.g. /jetpack/v4/widget-modules) where is_admin() is false. The render + // requests (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render // pieces here self-gate on admin_init, so loading them globally is inert // off the dashboard. Only the polyfill registration below is admin-scoped. $build_entry = __DIR__ . '/../build/build.php'; diff --git a/projects/packages/premium-analytics/src/dashboard-layout.php b/projects/packages/premium-analytics/src/dashboard-layout.php index e230774b30e6..d0bf1e77934a 100644 --- a/projects/packages/premium-analytics/src/dashboard-layout.php +++ b/projects/packages/premium-analytics/src/dashboard-layout.php @@ -20,6 +20,7 @@ namespace Automattic\Jetpack\PremiumAnalytics; require_once __DIR__ . '/dashboard-grammar.php'; +require_once __DIR__ . '/rest-namespace.php'; /** * Preferences scope under which the dashboard layout is stored. Mirrors the @@ -44,11 +45,6 @@ */ const DASHBOARD_NAME = 'jetpack-premium-analytics_dashboard'; -/** - * REST namespace that exposes the dashboard's default layout. - */ -const DASHBOARD_REST_NAMESPACE = 'jetpack/v4'; - /** * Filter through which the default layout for a dashboard is resolved. */ diff --git a/projects/packages/premium-analytics/src/rest-namespace.php b/projects/packages/premium-analytics/src/rest-namespace.php new file mode 100644 index 000000000000..9f853bc44b42 --- /dev/null +++ b/projects/packages/premium-analytics/src/rest-namespace.php @@ -0,0 +1,13 @@ + \WP_REST_Server::READABLE, diff --git a/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts b/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts new file mode 100644 index 000000000000..fc7c9af3caf4 --- /dev/null +++ b/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts @@ -0,0 +1,10 @@ +/** + * Internal dependencies + */ +import { DASHBOARD_REST_NAMESPACE } from '../../routes/dashboard/hooks/constants'; + +describe( 'Premium Analytics dashboard REST namespace', () => { + it( 'uses the WPCOM namespace for dashboard endpoints', () => { + expect( DASHBOARD_REST_NAMESPACE ).toBe( 'wpcom/v2' ); + } ); +} ); diff --git a/projects/packages/premium-analytics/tests/php/Dashboard_Layout_Test.php b/projects/packages/premium-analytics/tests/php/Dashboard_Layout_Test.php index 7c3daa99a79f..0ce1cad7cf35 100644 --- a/projects/packages/premium-analytics/tests/php/Dashboard_Layout_Test.php +++ b/projects/packages/premium-analytics/tests/php/Dashboard_Layout_Test.php @@ -8,6 +8,7 @@ namespace Automattic\Jetpack\PremiumAnalytics; use PHPUnit\Framework\TestCase; +use WP_REST_Server; require_once __DIR__ . '/../../src/dashboard-layout.php'; @@ -16,6 +17,37 @@ */ class Dashboard_Layout_Test extends TestCase { + const ROUTE = '/wpcom/v2/dashboards/(?P[a-z][a-z0-9-]*(?:_[a-z0-9-]+)*)/default-layout'; + const LEGACY_ROUTE = '/jetpack/v4/dashboards/(?P[a-z][a-z0-9-]*(?:_[a-z0-9-]+)*)/default-layout'; + + /** + * Reset REST globals after route registration tests. + */ + protected function tearDown(): void { + global $wp_rest_server; + $wp_rest_server = null; + parent::tearDown(); + } + + /** + * The default-layout route uses the WPCOM namespace. + */ + public function test_default_layout_route_uses_wpcom_v2_namespace() { + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + + if ( false === has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ) ) { + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); + } + + do_action( 'rest_api_init' ); + + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( self::ROUTE, $routes ); + $this->assertArrayNotHasKey( self::LEGACY_ROUTE, $routes ); + } + /** * Non-Premium-Analytics dashboards are left untouched. */ diff --git a/projects/packages/premium-analytics/tests/php/Dashboard_Section_Test.php b/projects/packages/premium-analytics/tests/php/Dashboard_Section_Test.php index b2b8dc434c5e..12ea2baa2ca5 100644 --- a/projects/packages/premium-analytics/tests/php/Dashboard_Section_Test.php +++ b/projects/packages/premium-analytics/tests/php/Dashboard_Section_Test.php @@ -220,7 +220,7 @@ public function test_accepts_dashboard_names_without_underscores() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/analytics/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/analytics/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -245,7 +245,7 @@ public function test_sections_route_returns_empty_array_for_unknown_dashboard() $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/unregistered_dashboard/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/unregistered_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -474,7 +474,7 @@ public function test_sections_route_returns_available_sections_sorted_by_order() $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_sections_dashboard/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_sections_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -523,7 +523,7 @@ public function test_sections_route_resolves_default_layouts() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_sections_dashboard/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_sections_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -571,7 +571,7 @@ public function test_sections_route_resolves_customized_empty_layouts() { ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_sections_dashboard/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_sections_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -596,7 +596,7 @@ public function test_sections_route_requires_manage_options() { wp_set_current_user( 0 ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_sections_dashboard/sections' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_sections_dashboard/sections' ) ); $this->assertSame( 401, $response->get_status() ); @@ -628,7 +628,7 @@ public function test_default_layout_route_returns_section_default_layout() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/default-layout' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/default-layout' ) ); $this->assertSame( 200, $response->get_status() ); @@ -642,7 +642,7 @@ public function test_default_layout_route_returns_404_for_unknown_section() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/missing/default-layout' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/missing/default-layout' ) ); $this->assertSame( 404, $response->get_status() ); @@ -666,7 +666,7 @@ public function test_default_layout_route_returns_404_for_unavailable_section() $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'GET', '/jetpack/v4/dashboards/route_unavailable_dashboard/sections/analytics/insights/default-layout' ) + new WP_REST_Request( 'GET', '/wpcom/v2/dashboards/route_unavailable_dashboard/sections/analytics/insights/default-layout' ) ); $this->assertSame( 404, $response->get_status() ); @@ -703,7 +703,7 @@ public function test_update_layout_route_persists_current_user_layout() { $user_id = $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', $custom_layout ); $response = rest_get_server()->dispatch( $request ); @@ -750,7 +750,7 @@ public function test_update_layout_route_does_not_persist_injected_dashboard_lay $user_id = $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', $custom_layout ); $response = rest_get_server()->dispatch( $request ); @@ -790,7 +790,7 @@ public function test_update_layout_route_persists_empty_layout_as_custom() { $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', array() ); $response = rest_get_server()->dispatch( $request ); @@ -823,7 +823,7 @@ public function test_update_layout_route_rejects_invalid_layout() { $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', array( @@ -853,7 +853,7 @@ public function test_update_layout_route_rejects_invalid_attributes() { $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', array( @@ -885,7 +885,7 @@ public function test_update_layout_route_requires_manage_options() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ); $request->set_param( 'layout', array() ); $response = rest_get_server()->dispatch( $request ); @@ -899,7 +899,7 @@ public function test_update_layout_route_requires_manage_options() { public function test_update_layout_route_returns_404_for_unknown_section() { $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/missing/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/missing/layout' ); $request->set_param( 'layout', array() ); $response = rest_get_server()->dispatch( $request ); @@ -924,7 +924,7 @@ public function test_update_layout_route_returns_404_for_unavailable_section() { $this->set_admin_user(); - $request = new WP_REST_Request( 'PUT', '/jetpack/v4/dashboards/route_unavailable_dashboard/sections/analytics/insights/layout' ); + $request = new WP_REST_Request( 'PUT', '/wpcom/v2/dashboards/route_unavailable_dashboard/sections/analytics/insights/layout' ); $request->set_param( 'layout', array() ); $response = rest_get_server()->dispatch( $request ); @@ -969,7 +969,7 @@ public function test_delete_layout_route_resets_to_default_layout() { ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/traffic/layout' ) ); $this->assertSame( 200, $response->get_status() ); @@ -997,7 +997,7 @@ public function test_delete_layout_route_returns_404_for_unknown_section() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_layout_dashboard/sections/analytics/missing/layout' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_layout_dashboard/sections/analytics/missing/layout' ) ); $this->assertSame( 404, $response->get_status() ); @@ -1021,7 +1021,7 @@ public function test_delete_layout_route_returns_404_for_unavailable_section() { $this->set_admin_user(); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_unavailable_dashboard/sections/analytics/insights/layout' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_unavailable_dashboard/sections/analytics/insights/layout' ) ); $this->assertSame( 404, $response->get_status() ); @@ -1086,7 +1086,7 @@ public function test_delete_sections_route_resets_all_section_layouts() { ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_layout_dashboard/sections' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_layout_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -1148,7 +1148,7 @@ public function test_delete_sections_route_removes_empty_dashboard_scope() { ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_layout_dashboard/sections' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_layout_dashboard/sections' ) ); $this->assertSame( 200, $response->get_status() ); @@ -1165,7 +1165,7 @@ public function test_delete_sections_route_requires_manage_options() { wp_set_current_user( 0 ); $response = rest_get_server()->dispatch( - new WP_REST_Request( 'DELETE', '/jetpack/v4/dashboards/route_layout_dashboard/sections' ) + new WP_REST_Request( 'DELETE', '/wpcom/v2/dashboards/route_layout_dashboard/sections' ) ); $this->assertSame( 401, $response->get_status() ); diff --git a/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php new file mode 100644 index 000000000000..d603c588ae20 --- /dev/null +++ b/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php @@ -0,0 +1,50 @@ +get_routes(); + + $this->assertArrayHasKey( self::ROUTE, $routes ); + $this->assertArrayNotHasKey( self::LEGACY_ROUTE, $routes ); + } +} From e58cc6f840334b11c6dedf3fe0ee3134075626d8 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Wed, 8 Jul 2026 19:20:53 +1200 Subject: [PATCH 09/34] Premium Analytics: Build WPCOM API paths on Simple --- .../api/__tests__/stats-proxy-fetch.test.ts | 77 +++++++++++++++++++ .../packages/data/src/api/constants.ts | 38 +++++++++ .../report-bookings-fetch.ts | 4 +- .../report-conversion-rate-fetch.ts | 4 +- .../report-coupons-by-date-fetch.ts | 4 +- .../report-coupons-fetch.ts | 4 +- .../report-customers-by-date-fetch.ts | 4 +- .../report-customers-fetch.ts | 4 +- ...port-order-attribution-by-product-fetch.ts | 4 +- .../report-order-attribution-summary-fetch.ts | 7 +- .../report-orders-fetch.ts | 6 +- .../report-products-fetch.ts | 4 +- .../report-sessions-by-device-fetch.ts | 4 +- .../report-visitors-by-location-fetch.ts | 8 +- .../report-visitors-fetch.ts | 4 +- .../data/src/api/stats-proxy-fetch.ts | 47 +++++++++-- .../queries/__tests__/stats-queries.test.ts | 34 ++++++++ .../src/queries/stats-app-notices-query.ts | 7 +- 18 files changed, 225 insertions(+), 39 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts index a2c2daf8057b..c5c4db0157cc 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts @@ -17,8 +17,23 @@ beforeEach( () => { afterEach( () => { jest.clearAllMocks(); + delete window.JetpackScriptData; } ); +function setSimpleScriptData( blogId = 191664832 ) { + Object.defineProperty( window, 'JetpackScriptData', { + configurable: true, + value: { + site: { + host: 'wpcom', + wpcom: { + blog_id: blogId, + }, + }, + }, + } ); +} + describe( 'getStatsProxyPath', () => { it( 'builds a v1.1 stats proxy path', () => { expect( @@ -61,6 +76,38 @@ describe( 'getStatsProxyPath', () => { } ) ).toBe( '/jetpack-premium-analytics/v1/proxy/v1.1/stats/visits?period=day' ); } ); + + it( 'builds Simple v1.1 stats paths for the WPCOM apiFetch bridge', () => { + setSimpleScriptData(); + + expect( + getStatsProxyPath( { + version: '1.1', + endpoint: 'stats/location-views/country', + params: { max: 10, period: 'day' }, + } ) + ).toBe( '/rest/v1.1/stats/location-views/country?max=10&period=day' ); + } ); + + it( 'builds Simple v2 paths for the WPCOM apiFetch bridge', () => { + setSimpleScriptData(); + + expect( + getStatsProxyPath( { + version: '2', + endpoint: 'analytics/reports/sessions/by-date', + params: { interval: 'day' }, + } ) + ).toBe( '/wpcom/v2/analytics/reports/sessions/by-date?interval=day' ); + } ); + + it( 'adds the current Simple site query for global upgrades requests', () => { + setSimpleScriptData( 67890 ); + + expect( getStatsProxyPath( { version: '1.2', endpoint: '/upgrades' } ) ).toBe( + '/rest/v1.2/upgrades?site=67890' + ); + } ); } ); describe( 'fetchStatsProxy', () => { @@ -93,4 +140,34 @@ describe( 'fetchStatsProxy', () => { data: body, } ); } ); + + it( 'uses WPCOM Simple paths directly before apiFetch middleware runs', async () => { + setSimpleScriptData(); + + await fetchStatsProxy( { + version: '1.1', + endpoint: 'stats/location-views/country', + params: { max: 10, period: 'day' }, + } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/rest/v1.1/stats/location-views/country?max=10&period=day', + method: 'GET', + } ); + } ); + + it( 'marks WPCOM Simple upgrades requests as global', async () => { + setSimpleScriptData( 67890 ); + + await fetchStatsProxy( { + version: '1.2', + endpoint: 'upgrades', + } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/rest/v1.2/upgrades?site=67890', + method: 'GET', + global: true, + } ); + } ); } ); diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts index 5f6e7b07dd5f..216ccd92b3b8 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/constants.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -3,3 +3,41 @@ */ export const statsProxyPath = '/jetpack-premium-analytics/v1/proxy'; export const reportsPath = `${ statsProxyPath }/v2/analytics/reports`; +export const wpcomSimpleReportsPath = '/wpcom/v2/analytics/reports'; +export const noticesPath = '/jetpack-premium-analytics/v1/notices'; +export const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; + +type JetpackScriptDataWindow = Window & { + JetpackScriptData?: { + site?: { + host?: string; + wpcom?: { + blog_id?: number | string; + }; + }; + }; +}; + +export function isWpcomSimpleSite() { + if ( typeof window === 'undefined' ) { + return false; + } + + return ( window as JetpackScriptDataWindow ).JetpackScriptData?.site?.host === 'wpcom'; +} + +export function getWpcomBlogId() { + if ( typeof window === 'undefined' ) { + return undefined; + } + + return ( window as JetpackScriptDataWindow ).JetpackScriptData?.site?.wpcom?.blog_id; +} + +export function getReportsPath() { + return isWpcomSimpleSite() ? wpcomSimpleReportsPath : reportsPath; +} + +export function getNoticesPath() { + return isWpcomSimpleSite() ? wpcomSimpleNoticesPath : noticesPath; +} diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts index 07bf13150694..f011fee46cc9 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -44,7 +44,7 @@ export async function fetchReportBookings( { filters, date_type, }: RequestReportBookingsParams ): Promise< ReportsBookingsByDateResponse > { - const apiUrl = `${ reportsPath }/bookings/by-date`; + const apiUrl = `${ getReportsPath() }/bookings/by-date`; const path = addQueryArgs( apiUrl, { from, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts index 3ea3b85788ca..61a72e1e9b54 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -45,7 +45,7 @@ export async function fetchReportConversionRate( { interval, filters, }: RequestReportConversionRateParams ): Promise< ReportsConversionRateByDateResponse > { - const path = addQueryArgs( `${ reportsPath }/sessions/by-conversion-rate`, { + const path = addQueryArgs( `${ getReportsPath() }/sessions/by-conversion-rate`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts index d969b017c97e..93c648ec2022 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -55,7 +55,7 @@ export async function fetchReportCouponsByDate( { filters, date_type, }: RequestReportCouponsByDateParams ): Promise< ReportsCouponsByDateResponse > { - const path = addQueryArgs( `${ reportsPath }/coupons/by-date`, { + const path = addQueryArgs( `${ getReportsPath() }/coupons/by-date`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts index db947d7bfabe..e263758b724d 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -41,7 +41,7 @@ export async function fetchReportCoupons( { filters, date_type, }: RequestReportCouponsParams ): Promise< ReportsCouponsResponse > { - const path = addQueryArgs( `${ reportsPath }/coupons/`, { + const path = addQueryArgs( `${ getReportsPath() }/coupons/`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts index 897ec6fff200..bfade9b6ed60 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { BaseReportParams } from '../../utils/types'; type ReportsCustomersByDateSummary = { @@ -66,7 +66,7 @@ export async function fetchReportCustomersByDate( { interval, date_type, }: RequestReportCustomersByDateParams ): Promise< ReportsCustomersByDateResponse > { - const path = addQueryArgs( `${ reportsPath }/customers/by-date`, { + const path = addQueryArgs( `${ getReportsPath() }/customers/by-date`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts index 77c6c6ac4df9..6cb6b0567182 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -40,7 +40,7 @@ export async function fetchReportCustomers( { filters, date_type, }: RequestReportCustomersParams ): Promise< ReportsCustomersNewReturningResponse > { - const path = addQueryArgs( `${ reportsPath }/customers/new-returning`, { + const path = addQueryArgs( `${ getReportsPath() }/customers/new-returning`, { from, to, filters, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts index 7e35aff2130e..67e8b3f25953 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; import type { ORDER_ATTRIBUTION_VIEWS } from '../report-order-attribution-summary-fetch/report-order-attribution-summary-fetch'; @@ -68,7 +68,7 @@ export async function fetchReportOrderAttributionByProduct( } const path = addQueryArgs( - `${ reportsPath }/order-attribution-by-product/${ view }/summary`, + `${ getReportsPath() }/order-attribution-by-product/${ view }/summary`, queryParams ); diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts index 6c62c763f539..d52ee9e58c3f 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { BaseReportParams } from '../../utils/types'; export const ORDER_ATTRIBUTION_VIEWS = [ @@ -79,7 +79,10 @@ export async function fetchReportOrderAttributionSummary( date_type, }; - const path = addQueryArgs( `${ reportsPath }/order-attribution/${ view }/summary`, queryParams ); + const path = addQueryArgs( + `${ getReportsPath() }/order-attribution/${ view }/summary`, + queryParams + ); return apiFetch< OrderAttributionSummaryResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts index 30ed3f40c56c..66c130b60b43 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts @@ -7,7 +7,7 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import { hasProductFilters } from '../../utils/product-filters'; -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -53,8 +53,8 @@ export async function fetchReportOrders( { }: RequestReportOrdersParams ): Promise< ReportsOrdersByDateResponse > { const hasProductFiltersValue = hasProductFilters( filters ); const apiUrl = hasProductFiltersValue - ? `${ reportsPath }/orders-by-product-type/by-date` - : `${ reportsPath }/orders/by-date`; + ? `${ getReportsPath() }/orders-by-product-type/by-date` + : `${ getReportsPath() }/orders/by-date`; const path = addQueryArgs( apiUrl, { from, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts index 19cef5031d8a..91b0c332abd2 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -7,7 +7,7 @@ import { addQueryArgs } from '@wordpress/url'; * Internal dependencies */ import { BaseReportParams } from '../../utils/types'; -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; export type RequestReportProductsParams = Omit< BaseReportParams, 'interval' > & { @@ -67,6 +67,6 @@ export async function fetchReportProducts( } return apiFetch< ReportProductsResponse >( { - path: addQueryArgs( `${ reportsPath }/products`, queryArgs ), + path: addQueryArgs( `${ getReportsPath() }/products`, queryArgs ), } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts index 3909742411dc..4f93fb5f40f8 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { BaseReportParams } from '../../utils/types'; /** @@ -51,7 +51,7 @@ export async function fetchReportSessionsByDevice( { from, to, }: RequestReportSessionsByDeviceParams ): Promise< ReportsSessionsByDeviceResponse > { - const path = addQueryArgs( `${ reportsPath }/sessions/by-device`, { + const path = addQueryArgs( `${ getReportsPath() }/sessions/by-device`, { from, to, } ); diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts index 3f1e2073cfc2..f71ee2b4abc8 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { BaseReportParams } from '../../utils/types'; type VisitorsByLocationReportDataItem = { @@ -36,8 +36,8 @@ export type RequestReportVisitorsByLocationParams = BaseReportParams & { /** * Fetch visitors grouped by location (country or region) for the selected period. * - * This endpoint is proxied through `/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/...` - * and ultimately served by wpcom analytics. + * This endpoint is served through the Premium Analytics proxy on Jetpack sites + * and directly through WPCOM public-api paths on Simple. */ export async function fetchReportVisitorsByLocation( { from, @@ -47,7 +47,7 @@ export async function fetchReportVisitorsByLocation( { country_code, limit, }: RequestReportVisitorsByLocationParams ): Promise< ReportsVisitorsByLocationResponse > { - const path = addQueryArgs( `${ reportsPath }/sessions/by-location`, { + const path = addQueryArgs( `${ getReportsPath() }/sessions/by-location`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts index a4a926a24469..85fd8cbc25ed 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { reportsPath } from '../constants'; +import { getReportsPath } from '../constants'; import type { BaseReportParams } from '../../utils/types'; type ReportsVisitorsByDateSummary = { @@ -32,7 +32,7 @@ export async function fetchReportVisitors( { to, interval, }: RequestReportVisitorsParams ): Promise< ReportsVisitorsByDateResponse > { - const path = addQueryArgs( `${ reportsPath }/sessions/by-date`, { + const path = addQueryArgs( `${ getReportsPath() }/sessions/by-date`, { from, to, interval, diff --git a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts index f31936c03303..8dacd01c8a81 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts @@ -6,7 +6,7 @@ import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { statsProxyPath } from './constants'; +import { getWpcomBlogId, isWpcomSimpleSite, statsProxyPath } from './constants'; export type StatsProxyVersion = '1.1' | '1.2' | '2'; @@ -41,13 +41,46 @@ function cleanQueryParams( params?: StatsProxyParams ) { return Object.keys( cleaned ).length ? cleaned : undefined; } +function isGlobalWpcomSimpleEndpoint( endpoint: string ) { + return normalizeEndpoint( endpoint ) === 'upgrades'; +} + +function getBasePath( version: StatsProxyVersion ) { + if ( ! isWpcomSimpleSite() ) { + return `${ statsProxyPath }/v${ version }`; + } + + return version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; +} + +function addWpcomSimpleSiteQuery( + params: StatsProxyParams | undefined +): StatsProxyParams | undefined { + if ( ! isWpcomSimpleSite() ) { + return params; + } + + const blogId = getWpcomBlogId(); + if ( ! blogId || params?.site ) { + return params; + } + + return { + ...params, + site: blogId, + }; +} + export function getStatsProxyPath( { version, endpoint, params, }: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > ) { - const path = `${ statsProxyPath }/v${ version }/${ normalizeEndpoint( endpoint ) }`; - const queryParams = cleanQueryParams( params ); + const normalizedEndpoint = normalizeEndpoint( endpoint ); + const path = `${ getBasePath( version ) }/${ normalizedEndpoint }`; + const queryParams = cleanQueryParams( + isGlobalWpcomSimpleEndpoint( normalizedEndpoint ) ? addWpcomSimpleSiteQuery( params ) : params + ); return queryParams ? addQueryArgs( path, queryParams ) : path; } @@ -60,10 +93,12 @@ export async function fetchStatsProxy< TResponse = unknown, TBody = unknown >( { body, }: StatsProxyFetchParams< TBody > ): Promise< TResponse > { const path = getStatsProxyPath( { version, endpoint, params } ); - - return apiFetch< TResponse >( { + const fetchOptions = { path, method, ...( method === 'POST' ? { data: body } : {} ), - } ); + ...( isWpcomSimpleSite() && isGlobalWpcomSimpleEndpoint( endpoint ) ? { global: true } : {} ), + }; + + return apiFetch< TResponse >( fetchOptions ); } diff --git a/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts b/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts index 8a70e081d7fe..c50eaaae2f9b 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts @@ -53,11 +53,26 @@ jest.mock( '@wordpress/api-fetch' ); const mockApiFetch = apiFetch as jest.MockedFunction< typeof apiFetch >; +function setSimpleScriptData() { + Object.defineProperty( window, 'JetpackScriptData', { + configurable: true, + value: { + site: { + host: 'wpcom', + }, + }, + } ); +} + describe( 'Stats query factories', () => { beforeEach( () => { mockApiFetch.mockReset(); } ); + afterEach( () => { + delete window.JetpackScriptData; + } ); + it( 'disables report queries until a date range is available', () => { expect( statsTopPostsQuery( {} as StatsReportParams ).enabled ).toBe( false ); } ); @@ -640,6 +655,25 @@ describe( 'Stats query factories', () => { } ); } ); + it( 'updates app notices through the WPCOM Stats endpoint on Simple', async () => { + setSimpleScriptData(); + mockApiFetch.mockResolvedValue( { opt_in_new_stats: false } ); + + await updateStatsAppNotice( { + id: 'opt_in_new_stats', + status: 'dismissed', + } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/wpcom/v2/jetpack-stats-dashboard/notices', + method: 'POST', + data: { + id: 'opt_in_new_stats', + status: 'dismissed', + }, + } ); + } ); + it( 'builds highlights query keys with endpoint params and sanitizer', () => { const query = statsHighlightsQuery( { source: 'stats-feedback' } ); diff --git a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts index 62351e3f5b27..457e712b46d7 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts @@ -1,10 +1,9 @@ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; +import { getNoticesPath } from '../api/constants'; import { statsAppQueryKeyPart } from './stats-app-query'; import type { UseQueryOptions } from '@tanstack/react-query'; -const noticesPath = '/jetpack-premium-analytics/v1/notices'; - export type StatsAppNoticeId = | 'able_to_submit_user_feedback' | 'client_free_plan_purchase_success' @@ -35,7 +34,7 @@ export const statsAppNoticesQuery = ( queryKey: [ 'stats-app', 'notices', statsAppQueryKeyPart( params ) ], queryFn: () => apiFetch< StatsAppNotices >( { - path: addQueryArgs( noticesPath, params ), + path: addQueryArgs( getNoticesPath(), params ), } ), placeholderData: previousData => previousData, } ); @@ -50,7 +49,7 @@ export type StatsAppNoticeMutationResponse = StatsAppNotices; export const updateStatsAppNotice = ( data: StatsAppNoticeMutationParams ) => apiFetch< StatsAppNoticeMutationResponse >( { - path: noticesPath, + path: getNoticesPath(), method: 'POST', data, } ); From bee2c77d03266649a2dbf4e41e52c74c62ad250a Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 10:53:14 +1200 Subject: [PATCH 10/34] Premium Analytics: Consolidate Simple transport in the data layer All Simple-aware path building now lives in one resolver in the data package, using the shared jetpack-script-data helpers. The init package's fallback apiFetch rewrite middleware is removed: every Premium Analytics request already flows through the data helpers, and sandbox testing showed middleware rewriting can run after WPCOM's own path scoping. --- .../packages/data/package.json | 1 + .../api/__tests__/stats-proxy-fetch.test.ts | 8 ++ .../packages/data/src/api/constants.ts | 41 ++----- .../data/src/api/stats-proxy-fetch.ts | 92 +++++++++------- .../__tests__/wpcom-simple-api-fetch.test.ts | 93 ---------------- .../packages/init/src/index.ts | 5 - .../init/src/wpcom-simple-api-fetch.ts | 103 ------------------ .../premium-analytics/src/class-analytics.php | 4 +- .../tests/js/dashboard-rest-namespace.test.ts | 10 -- 9 files changed, 75 insertions(+), 282 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts delete mode 100644 projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts delete mode 100644 projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts diff --git a/projects/packages/premium-analytics/packages/data/package.json b/projects/packages/premium-analytics/packages/data/package.json index 9a16293218f1..0f600184af20 100644 --- a/projects/packages/premium-analytics/packages/data/package.json +++ b/projects/packages/premium-analytics/packages/data/package.json @@ -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", diff --git a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts index c5c4db0157cc..8a7683c29eb9 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts @@ -108,6 +108,14 @@ describe( 'getStatsProxyPath', () => { '/rest/v1.2/upgrades?site=67890' ); } ); + + it( 'does not overwrite an explicit upgrades site query on Simple', () => { + setSimpleScriptData( 67890 ); + + expect( + getStatsProxyPath( { version: '1.2', endpoint: '/upgrades', params: { site: 41 } } ) + ).toBe( '/rest/v1.2/upgrades?site=41' ); + } ); } ); describe( 'fetchStatsProxy', () => { diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts index 216ccd92b3b8..2e8e9e945ea8 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/constants.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -1,43 +1,24 @@ +/** + * External dependencies + */ +import { isSimpleSite } from '@automattic/jetpack-script-data'; + /** * Constants for API endpoints */ export const statsProxyPath = '/jetpack-premium-analytics/v1/proxy'; export const reportsPath = `${ statsProxyPath }/v2/analytics/reports`; -export const wpcomSimpleReportsPath = '/wpcom/v2/analytics/reports'; export const noticesPath = '/jetpack-premium-analytics/v1/notices'; -export const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; - -type JetpackScriptDataWindow = Window & { - JetpackScriptData?: { - site?: { - host?: string; - wpcom?: { - blog_id?: number | string; - }; - }; - }; -}; - -export function isWpcomSimpleSite() { - if ( typeof window === 'undefined' ) { - return false; - } - return ( window as JetpackScriptDataWindow ).JetpackScriptData?.site?.host === 'wpcom'; -} - -export function getWpcomBlogId() { - if ( typeof window === 'undefined' ) { - return undefined; - } - - return ( window as JetpackScriptDataWindow ).JetpackScriptData?.site?.wpcom?.blog_id; -} +// WPCOM Simple has no local proxy or notices endpoint: requests use public-api +// namespaces directly, dispatched by WPCOM's wp-admin apiFetch bridge. +export const wpcomSimpleReportsPath = '/wpcom/v2/analytics/reports'; +export const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; export function getReportsPath() { - return isWpcomSimpleSite() ? wpcomSimpleReportsPath : reportsPath; + return isSimpleSite() ? wpcomSimpleReportsPath : reportsPath; } export function getNoticesPath() { - return isWpcomSimpleSite() ? wpcomSimpleNoticesPath : noticesPath; + return isSimpleSite() ? wpcomSimpleNoticesPath : noticesPath; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts index 8dacd01c8a81..28309d140943 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts @@ -1,12 +1,13 @@ /** * External dependencies */ +import { getScriptData, isSimpleSite } from '@automattic/jetpack-script-data'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getWpcomBlogId, isWpcomSimpleSite, statsProxyPath } from './constants'; +import { statsProxyPath } from './constants'; export type StatsProxyVersion = '1.1' | '1.2' | '2'; @@ -25,6 +26,11 @@ export type StatsProxyFetchParams< TBody = unknown > = { body?: TBody; }; +type ResolvedStatsProxyRequest = { + path: string; + global?: boolean; +}; + function normalizeEndpoint( endpoint: string ) { return endpoint.replace( /^\/+/, '' ); } @@ -41,48 +47,57 @@ function cleanQueryParams( params?: StatsProxyParams ) { return Object.keys( cleaned ).length ? cleaned : undefined; } -function isGlobalWpcomSimpleEndpoint( endpoint: string ) { - return normalizeEndpoint( endpoint ) === 'upgrades'; -} +/** + * Resolve the request for a stats endpoint in the current environment. + * + * Connected Jetpack sites reach WPCOM through the local Premium Analytics + * proxy. WPCOM Simple has no local proxy: requests use public-api namespaces + * directly and are dispatched by WPCOM's wp-admin apiFetch bridge, which + * site-scopes every path unless the request is marked `global`. `upgrades` + * is a global public-api endpoint, so it opts out of the path scoping and + * carries the site as a query arg instead. + * + * @param request - The stats request to resolve. + * @param request.version - WPCOM API version (`1.1`, `1.2`, or `2`). + * @param request.endpoint - Endpoint path below the version base. + * @param request.params - Query params of the request. + * @return The apiFetch path, plus `global: true` for global WPCOM endpoints. + */ +function resolveStatsProxyRequest( { + version, + endpoint, + params, +}: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > ): ResolvedStatsProxyRequest { + const normalizedEndpoint = normalizeEndpoint( endpoint ); -function getBasePath( version: StatsProxyVersion ) { - if ( ! isWpcomSimpleSite() ) { - return `${ statsProxyPath }/v${ version }`; + if ( ! isSimpleSite() ) { + const path = `${ statsProxyPath }/v${ version }/${ normalizedEndpoint }`; + const queryParams = cleanQueryParams( params ); + return { path: queryParams ? addQueryArgs( path, queryParams ) : path }; } - return version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; -} - -function addWpcomSimpleSiteQuery( - params: StatsProxyParams | undefined -): StatsProxyParams | undefined { - if ( ! isWpcomSimpleSite() ) { - return params; - } + const base = version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; + const path = `${ base }/${ normalizedEndpoint }`; - const blogId = getWpcomBlogId(); - if ( ! blogId || params?.site ) { - return params; + if ( normalizedEndpoint !== 'upgrades' ) { + const queryParams = cleanQueryParams( params ); + return { path: queryParams ? addQueryArgs( path, queryParams ) : path }; } + const blogId = getScriptData()?.site?.wpcom?.blog_id; + const queryParams = cleanQueryParams( + blogId && ! params?.site ? { ...params, site: blogId } : params + ); return { - ...params, - site: blogId, + path: queryParams ? addQueryArgs( path, queryParams ) : path, + global: true, }; } -export function getStatsProxyPath( { - version, - endpoint, - params, -}: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > ) { - const normalizedEndpoint = normalizeEndpoint( endpoint ); - const path = `${ getBasePath( version ) }/${ normalizedEndpoint }`; - const queryParams = cleanQueryParams( - isGlobalWpcomSimpleEndpoint( normalizedEndpoint ) ? addWpcomSimpleSiteQuery( params ) : params - ); - - return queryParams ? addQueryArgs( path, queryParams ) : path; +export function getStatsProxyPath( + request: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > +) { + return resolveStatsProxyRequest( request ).path; } export async function fetchStatsProxy< TResponse = unknown, TBody = unknown >( { @@ -92,13 +107,12 @@ export async function fetchStatsProxy< TResponse = unknown, TBody = unknown >( { method = 'GET', body, }: StatsProxyFetchParams< TBody > ): Promise< TResponse > { - const path = getStatsProxyPath( { version, endpoint, params } ); - const fetchOptions = { + const { path, global: isGlobal } = resolveStatsProxyRequest( { version, endpoint, params } ); + + return apiFetch< TResponse >( { path, method, ...( method === 'POST' ? { data: body } : {} ), - ...( isWpcomSimpleSite() && isGlobalWpcomSimpleEndpoint( endpoint ) ? { global: true } : {} ), - }; - - return apiFetch< TResponse >( fetchOptions ); + ...( isGlobal ? { global: true } : {} ), + } ); } diff --git a/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts b/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts deleted file mode 100644 index d7dc90838089..000000000000 --- a/projects/packages/premium-analytics/packages/init/src/__tests__/wpcom-simple-api-fetch.test.ts +++ /dev/null @@ -1,93 +0,0 @@ -/** - * Internal dependencies - */ -import { getWpcomSimpleApiFetchOptions } from '../wpcom-simple-api-fetch'; - -/** - * Set minimal script data for WPCOM Simple apiFetch tests. - * - * @param blogId - WPCOM blog ID. - */ -function setScriptData( blogId = 12345 ) { - Object.defineProperty( window, 'JetpackScriptData', { - configurable: true, - value: { - site: { - host: 'wpcom', - wpcom: { - blog_id: blogId, - }, - }, - }, - } ); -} - -afterEach( () => { - delete window.JetpackScriptData; -} ); - -describe( 'getWpcomSimpleApiFetchOptions', () => { - it( 'rewrites v2 proxy requests to wpcom v2 paths', () => { - expect( - getWpcomSimpleApiFetchOptions( { - path: '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/sessions/by-date?interval=day', - method: 'GET', - } ) - ).toEqual( { - path: '/wpcom/v2/analytics/reports/sessions/by-date?interval=day', - method: 'GET', - } ); - } ); - - it( 'rewrites v1 proxy requests to rest versioned paths', () => { - expect( - getWpcomSimpleApiFetchOptions( { - path: '/jetpack-premium-analytics/v1/proxy/v1.1/stats/visits?period=day', - } ) - ).toEqual( { - path: '/rest/v1.1/stats/visits?period=day', - } ); - } ); - - it( 'rewrites notices to the WPCOM Stats notices endpoint', () => { - expect( - getWpcomSimpleApiFetchOptions( { - path: '/jetpack-premium-analytics/v1/notices?force_refresh=true', - } ) - ).toEqual( { - path: '/wpcom/v2/jetpack-stats-dashboard/notices?force_refresh=true', - } ); - } ); - - it( 'leaves non Premium Analytics paths alone', () => { - const options = { path: '/wp/v2/settings' }; - - expect( getWpcomSimpleApiFetchOptions( options ) ).toBe( options ); - } ); - - it( 'keeps upgrades global and adds the current site query', () => { - setScriptData( 67890 ); - - expect( - getWpcomSimpleApiFetchOptions( { - path: '/jetpack-premium-analytics/v1/proxy/v1.2/upgrades?type=transferred', - } ) - ).toEqual( { - path: '/rest/v1.2/upgrades?type=transferred&site=67890', - global: true, - } ); - } ); - - it( 'does not overwrite an explicit upgrades site query', () => { - setScriptData( 67890 ); - - expect( - getWpcomSimpleApiFetchOptions( { - path: '/jetpack-premium-analytics/v1/proxy/v1.2/upgrades?site=41', - } ) - ).toEqual( { - path: '/rest/v1.2/upgrades?site=41', - global: true, - } ); - } ); -} ); diff --git a/projects/packages/premium-analytics/packages/init/src/index.ts b/projects/packages/premium-analytics/packages/init/src/index.ts index 0f816a147210..4dd80aa0b7f7 100644 --- a/projects/packages/premium-analytics/packages/init/src/index.ts +++ b/projects/packages/premium-analytics/packages/init/src/index.ts @@ -6,10 +6,6 @@ import apiFetch from '@wordpress/api-fetch'; import { store as bootStore } from '@wordpress/boot'; import { dispatch } from '@wordpress/data'; import { chartBar } from '@wordpress/icons'; -/** - * Internal dependencies - */ -import { setupWpcomSimpleApiFetch } from './wpcom-simple-api-fetch'; // apiFetch middleware registers onto a shared, process-wide chain. Guard so // repeated init() calls (re-mount, HMR, a future second boot) don't stack @@ -45,7 +41,6 @@ function setupApiFetch(): void { */ export async function init(): Promise< void > { setupApiFetch(); - setupWpcomSimpleApiFetch(); dispatch( bootStore ).updateMenuItem( 'dashboard', { icon: chartBar, diff --git a/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts b/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts deleted file mode 100644 index 43ce78b59d14..000000000000 --- a/projects/packages/premium-analytics/packages/init/src/wpcom-simple-api-fetch.ts +++ /dev/null @@ -1,103 +0,0 @@ -/** - * External dependencies - */ -import { getScriptData, isSimpleSite } from '@automattic/jetpack-script-data'; -import apiFetch from '@wordpress/api-fetch'; - -type ApiFetchOptions = Parameters< typeof apiFetch >[ 0 ] & { - global?: boolean; -}; - -const localProxyPrefix = '/jetpack-premium-analytics/v1/proxy/'; -const localNoticesPath = '/jetpack-premium-analytics/v1/notices'; -const wpcomNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; - -let wpcomSimpleMiddlewareConfigured = false; - -/** - * Append the current site query required by global WPCOM endpoints. - * - * @param path - API path to update. - * @return API path with the current site query when available. - */ -function appendSiteQuery( path: string ): string { - if ( /([?&])site=/.test( path ) ) { - return path; - } - - const blogId = getScriptData()?.site?.wpcom?.blog_id; - if ( ! blogId ) { - return path; - } - - return `${ path }${ path.includes( '?' ) ? '&' : '?' }site=${ encodeURIComponent( blogId ) }`; -} - -/** - * Resolve the public-api namespace for a proxied request version. - * - * @param version - API version from the local proxy path. - * @return WPCOM public-api namespace path. - */ -function getWpcomApiBasePath( version: string ): string { - return version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; -} - -/** - * Rewrite local Premium Analytics proxy options for WPCOM Simple. - * - * @param options - apiFetch options. - * @return Rewritten apiFetch options when the request targets Premium Analytics. - */ -export function getWpcomSimpleApiFetchOptions( options: ApiFetchOptions ): ApiFetchOptions { - const path = options.path; - if ( typeof path !== 'string' ) { - return options; - } - - if ( path === localNoticesPath || path.startsWith( `${ localNoticesPath }?` ) ) { - return { - ...options, - path: `${ wpcomNoticesPath }${ path.slice( localNoticesPath.length ) }`, - }; - } - - if ( ! path.startsWith( localProxyPrefix ) ) { - return options; - } - - const proxyPath = path.slice( localProxyPrefix.length ); - const match = proxyPath.match( /^v([^/]+)(\/.*)?$/ ); - if ( ! match ) { - return options; - } - - const version = match[ 1 ]; - const endpointWithQuery = match[ 2 ] ?? ''; - const wpcomPath = `${ getWpcomApiBasePath( version ) }${ endpointWithQuery }`; - - if ( endpointWithQuery === '/upgrades' || endpointWithQuery.startsWith( '/upgrades?' ) ) { - return { - ...options, - path: appendSiteQuery( wpcomPath ), - global: true, - }; - } - - return { - ...options, - path: wpcomPath, - }; -} - -/** - * Register the WPCOM Simple apiFetch rewrite middleware. - */ -export function setupWpcomSimpleApiFetch(): void { - if ( wpcomSimpleMiddlewareConfigured || ! isSimpleSite() ) { - return; - } - - apiFetch.use( ( options, next ) => next( getWpcomSimpleApiFetchOptions( options ) ) ); - wpcomSimpleMiddlewareConfigured = true; -} diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 5febd82e8648..58c6a7fdaeb6 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -73,7 +73,7 @@ public static function init_wpcom_simple( $options = array() ) { * @param bool $register_local_services Whether to register connected-site local services. * @return void */ - private static function boot( $options = array(), $register_local_services = true ) { + private static function boot( $options, $register_local_services ) { if ( self::$initialized ) { return; } @@ -142,7 +142,7 @@ private static function boot( $options = array(), $register_local_services = tru // Load wp-build output (interceptor, modules, routes, page render). // Must stay above the is_admin() gate: build/widgets.php defines the // manifest the widget registry reads, and the registry serves REST - // requests (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render + // requests (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render // pieces here self-gate on admin_init, so loading them globally is inert // off the dashboard. Only the polyfill registration below is admin-scoped. $build_entry = __DIR__ . '/../build/build.php'; diff --git a/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts b/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts deleted file mode 100644 index fc7c9af3caf4..000000000000 --- a/projects/packages/premium-analytics/tests/js/dashboard-rest-namespace.test.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * Internal dependencies - */ -import { DASHBOARD_REST_NAMESPACE } from '../../routes/dashboard/hooks/constants'; - -describe( 'Premium Analytics dashboard REST namespace', () => { - it( 'uses the WPCOM namespace for dashboard endpoints', () => { - expect( DASHBOARD_REST_NAMESPACE ).toBe( 'wpcom/v2' ); - } ); -} ); From 04173153a75998a259c6bbe2fa647abee6c5e3de Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 10:53:34 +1200 Subject: [PATCH 11/34] Premium Analytics: Pin jest to UTC Date-window logic falls back to the machine timezone, so date tests only passed on UTC machines (CI). Pin the suite so results match CI everywhere. --- projects/packages/premium-analytics/tests/jest.config.cjs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/projects/packages/premium-analytics/tests/jest.config.cjs b/projects/packages/premium-analytics/tests/jest.config.cjs index 1720850fc917..bdecae129c2e 100644 --- a/projects/packages/premium-analytics/tests/jest.config.cjs +++ b/projects/packages/premium-analytics/tests/jest.config.cjs @@ -1,6 +1,10 @@ const path = require( 'path' ); const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); +// Date logic falls back to the machine timezone (Intl.DateTimeFormat), so +// date-window tests only pass in UTC. Pin it to match CI on any machine. +process.env.TZ = 'UTC'; + module.exports = { ...baseConfig, rootDir: path.join( __dirname, '..' ), From dfe0d0b32510853615ad43fbbaadfb3a840eec27 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 10:53:38 +1200 Subject: [PATCH 12/34] Premium Analytics: Drop composer versions overrides The path-repository versions pins are not needed for the monorepo solve: composer resolves feature branches to dev-trunk on its own, and the locks regenerate cleanly without them. Mirror/sandbox builds should be re-verified before merge. --- .../packages/jetpack-mu-wpcom/composer.json | 9 +-- .../packages/premium-analytics/composer.json | 9 +-- .../plugins/mu-wpcom-plugin/composer.lock | 4 +- .../plugins/premium-analytics/composer.lock | 2 +- projects/plugins/wpcomsh/composer.json | 9 +-- projects/plugins/wpcomsh/composer.lock | 78 +++++++++---------- 6 files changed, 45 insertions(+), 66 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json index 4b0372445253..0ffd78c77ed2 100644 --- a/projects/packages/jetpack-mu-wpcom/composer.json +++ b/projects/packages/jetpack-mu-wpcom/composer.json @@ -64,14 +64,7 @@ "type": "path", "url": "../../packages/*", "options": { - "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" - } + "monorepo": true } } ], diff --git a/projects/packages/premium-analytics/composer.json b/projects/packages/premium-analytics/composer.json index e5dccf9c5dcf..020a7855c061 100644 --- a/projects/packages/premium-analytics/composer.json +++ b/projects/packages/premium-analytics/composer.json @@ -46,14 +46,7 @@ "type": "path", "url": "../*", "options": { - "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" - } + "monorepo": true } } ], diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index 41e6b84f1bfe..403670dbe6e5 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1371,7 +1371,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "f5a0802d45daac83c0bf9df90cf6f330527227cf" + "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" }, "require": { "automattic/block-delimiter": "@dev", @@ -1727,7 +1727,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" + "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" }, "require": { "automattic/jetpack-assets": "@dev", diff --git a/projects/plugins/premium-analytics/composer.lock b/projects/plugins/premium-analytics/composer.lock index ad94511f0030..fc065d9d84c0 100644 --- a/projects/plugins/premium-analytics/composer.lock +++ b/projects/plugins/premium-analytics/composer.lock @@ -841,7 +841,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" + "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" }, "require": { "automattic/jetpack-assets": "@dev", diff --git a/projects/plugins/wpcomsh/composer.json b/projects/plugins/wpcomsh/composer.json index 4df53e7f4412..b5113604c94b 100644 --- a/projects/plugins/wpcomsh/composer.json +++ b/projects/plugins/wpcomsh/composer.json @@ -62,14 +62,7 @@ "type": "path", "url": "../../packages/*", "options": { - "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" - } + "monorepo": true } }, { diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index dc596b40ab29..622f4871f491 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "c9d4bc712b476604df193f64608b93ab", + "content-hash": "d0871f7a7395d1ee5864c7d40d2e35df", "packages": [ { "name": "automattic/block-delimiter", @@ -12,7 +12,7 @@ "dist": { "type": "path", "url": "../../packages/block-delimiter", - "reference": "bb5cd334fb6d00ed5d5c26f6f7cff9b3ae55ae99" + "reference": "e860d7feb6347341faad763d7e342cc6a5e8b449" }, "require": { "php": ">=7.2" @@ -123,7 +123,7 @@ "dist": { "type": "path", "url": "../../packages/a8c-mc-stats", - "reference": "003b710eb1eaf27ed970000b41a292c89fcfe5ba" + "reference": "48895090b425c307b50fdf77f2c770719e1d3cf4" }, "require": { "php": ">=7.2" @@ -176,7 +176,7 @@ "dist": { "type": "path", "url": "../../packages/admin-ui", - "reference": "edc5a654cdffde980eafb17c2c1d98ca755957ad" + "reference": "96398da45af93491fdd90cea96331e624b42e39e" }, "require": { "automattic/jetpack-redirect": "@dev", @@ -253,7 +253,7 @@ "dist": { "type": "path", "url": "../../packages/agents-manager", - "reference": "7a2c9058e35e94a42562eee55d0528376601801b" + "reference": "b4b58a385a26210a56620238aee1779e9373d195" }, "require": { "automattic/jetpack-assets": "@dev", @@ -328,7 +328,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "5b5ad5da3b83d4a98780a175d17df9fe61e58350" + "reference": "7214a03808294ae652164b2495dc4e8a09c54e89" }, "require": { "automattic/jetpack-constants": "@dev", @@ -401,7 +401,7 @@ "dist": { "type": "path", "url": "../../packages/autoloader", - "reference": "e38ca7a8dc245675a896e6f1e126bdf6b5157ba6" + "reference": "afee041b24440e62c6ddb502b3c6716044f8c3ed" }, "require": { "composer-plugin-api": "^2.2", @@ -469,7 +469,7 @@ "dist": { "type": "path", "url": "../../packages/blaze", - "reference": "6f33631a30fe59d482659c8ce80da39e349a0e83" + "reference": "66186f7bc5595357512833c76816e8496b6a6c0b" }, "require": { "automattic/jetpack-admin-ui": "@dev", @@ -545,7 +545,7 @@ "dist": { "type": "path", "url": "../../packages/blocks", - "reference": "22e2fd3dc418c457cd5113b9cc4e040966ec2128" + "reference": "21984233a8e0e6f3dd700bdaa35901f0f9c3162f" }, "require": { "automattic/jetpack-constants": "@dev", @@ -601,7 +601,7 @@ "dist": { "type": "path", "url": "../../packages/classic-theme-helper", - "reference": "94619701fa1a58360a0a7e0afe159bc2dd7e5095" + "reference": "de10e684e8c1cc0bdb66910f27aae22bdcfce75f" }, "require": { "automattic/jetpack-assets": "@dev", @@ -667,7 +667,7 @@ "dist": { "type": "path", "url": "../../packages/compat", - "reference": "08939574a981fd4dc944070d7a3948decd9ff37a" + "reference": "cca3c26e1aa14e6cfe16ed55099dd1fc39362778" }, "require": { "php": ">=7.2" @@ -701,7 +701,7 @@ "dist": { "type": "path", "url": "../../packages/composer-plugin", - "reference": "15799e1372ef4a4b2346310027df6bcef1c1ab87" + "reference": "4c4b86f8b4a330c7033d2f49e33397d044a3d7a0" }, "require": { "composer-plugin-api": "^2.2", @@ -761,7 +761,7 @@ "dist": { "type": "path", "url": "../../packages/config", - "reference": "639ed7b8e15b027b74ede1651392cde0bf4928f4" + "reference": "c57552fadeaa0ee131577eea652128b3686f62cc" }, "require": { "php": ">=7.2" @@ -832,7 +832,7 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "ac92fb106fb48e2bdfe0946da6e387b2f61585fb" + "reference": "ce362d9d8bf97228d5267f8d4a9b05379ff98aa2" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -920,7 +920,7 @@ "dist": { "type": "path", "url": "../../packages/constants", - "reference": "7cc5b2f7d1f65f32d8ee5c1d11b6df716fcb6db1" + "reference": "a2f68c778c6c47b3e9f7699379fe5efa8ffe2acc" }, "require": { "php": ">=7.2" @@ -974,7 +974,7 @@ "dist": { "type": "path", "url": "../../packages/device-detection", - "reference": "9ab4dcf177ba5b09afe97c13f6f538edf7ba5fd8" + "reference": "060e4e98b1fb7351b6635c5c8c16a0a2f365ce90" }, "require": { "php": ">=7.2" @@ -1095,7 +1095,7 @@ "dist": { "type": "path", "url": "../../packages/google-analytics", - "reference": "c03943f744703f6239ee5cc78fd5c29f2bc8de37" + "reference": "93748801f1647c1a443f8af82f79eac985303dfa" }, "require": { "automattic/jetpack-status": "@dev", @@ -1160,7 +1160,7 @@ "dist": { "type": "path", "url": "../../packages/image-cdn", - "reference": "ff73ec5136b5760ef6f0dc5dc7296a7a2fa70ae8" + "reference": "9e71f33ab5611baafd105ddf7200e13be88fe827" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1220,7 +1220,7 @@ "dist": { "type": "path", "url": "../../packages/ip", - "reference": "e487e4a8e010cf98cac25b18e858529de439dcb9" + "reference": "d63dccb293846fdd6ef54fb78848d043815de4bb" }, "require": { "php": ">=7.2" @@ -1278,7 +1278,7 @@ "dist": { "type": "path", "url": "../../packages/jitm", - "reference": "c32c102004bd39788ef69c33756e132659506751" + "reference": "9060a71b2749d8026a07f334efc6e536114a7ced" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -1353,7 +1353,7 @@ "dist": { "type": "path", "url": "../../packages/logo", - "reference": "8e9b400315ba6fe6e556332c20310fb635f16185" + "reference": "9772ff10ed4f2e82abda216a2631b547d0a562e9" }, "require": { "php": ">=7.2" @@ -1406,7 +1406,7 @@ "dist": { "type": "path", "url": "../../packages/masterbar", - "reference": "09ed33eb6a354978d8e5e1d41bfa737d7a329e40" + "reference": "2c9c34203668df2e980fdf37d8f871bac19b11c9" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1487,7 +1487,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "ce82f8a3e5609f987876b4af12a00e797b7ef709" + "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" }, "require": { "automattic/block-delimiter": "@dev", @@ -1583,7 +1583,7 @@ "dist": { "type": "path", "url": "../../packages/password-checker", - "reference": "8dd32a3ef5322b35a7caaaace68bd695605075cc" + "reference": "d9e7a7a8db710ebc8d064ae207775df8bd9e6662" }, "require": { "php": ">=7.2" @@ -1638,7 +1638,7 @@ "dist": { "type": "path", "url": "../../packages/plans", - "reference": "a6e564eabb06e64767288117ae86db0432ec1099" + "reference": "d694876b1987293d1f9c0de3ff6ae37d8be95e85" }, "require": { "automattic/jetpack-connection": "@dev", @@ -1776,7 +1776,7 @@ "dist": { "type": "path", "url": "../../packages/post-list", - "reference": "1bf8d892fc7ff9671e0fa02d9e2101b707c6eee0" + "reference": "1e835cbf7e005405652bee4c2f9460979de1bf92" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1841,7 +1841,7 @@ "dist": { "type": "path", "url": "../../packages/post-media", - "reference": "693f3207f6937211e1761f9d05b2864a74ff38ac" + "reference": "e1c691c5b9ca6debdbf8351aaf6658189f1c0aa6" }, "require": { "automattic/block-delimiter": "@dev", @@ -1908,7 +1908,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "b3511e8ca26dd4052147f368ebf1b0aa9179a787" + "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1980,7 +1980,7 @@ "dist": { "type": "path", "url": "../../packages/redirect", - "reference": "654ba6e8dda4889250e4e671134aeb0bc8755481" + "reference": "84a8949d203512d279ad65280af8d2af01d4d799" }, "require": { "automattic/jetpack-status": "@dev", @@ -2035,7 +2035,7 @@ "dist": { "type": "path", "url": "../../packages/roles", - "reference": "df8247a8970fdcc53b27916231a1ed5482f270f2" + "reference": "a9e558a67db231ecf3194a02125a4b3f14a48b94" }, "require": { "php": ">=7.2" @@ -2089,7 +2089,7 @@ "dist": { "type": "path", "url": "../../packages/rtc", - "reference": "62dcc023afd4f82da7ad6145e797122ab63091ad" + "reference": "cf9257a7cb581975e66e8cee0e5dc04d3d4da7a1" }, "require": { "automattic/jetpack-assets": "@dev", @@ -2155,7 +2155,7 @@ "dist": { "type": "path", "url": "../../packages/stats", - "reference": "e842f3502fce83a510a8115036d51edd9c5b1274" + "reference": "353dcc22a8de746e21bd2c6334d1ca7206fea704" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2222,7 +2222,7 @@ "dist": { "type": "path", "url": "../../packages/stats-admin", - "reference": "aa4d6effbd21f16943f4f5d4b80412c8711efc11" + "reference": "69020479c6220930ee90793a30b40ecade9de84d" }, "require": { "automattic/jetpack-blaze": "@dev", @@ -2290,7 +2290,7 @@ "dist": { "type": "path", "url": "../../packages/status", - "reference": "1d6ff66fe1a7869d8acef38cc32289aa3dbdd7c6" + "reference": "30e50be852b4f6b22a2a47ed7ab3ef6daa4074b6" }, "require": { "automattic/jetpack-constants": "@dev", @@ -2354,7 +2354,7 @@ "dist": { "type": "path", "url": "../../packages/sync", - "reference": "6b066f38821fc99458a762fe0626c2b03c2ed819" + "reference": "0e6c312d73cf8b2bde57a5656e16e42c1b593217" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2426,7 +2426,7 @@ "dist": { "type": "path", "url": "../../packages/wp-abilities", - "reference": "7af51269db986f7bde09d44d66b497c0709e8b74" + "reference": "97a1a9548ae11e0cd0f0514d81c0701474968828" }, "require": { "php": ">=7.2" @@ -2484,7 +2484,7 @@ "dist": { "type": "path", "url": "../../packages/wp-build-polyfills", - "reference": "552586a6a5288c977124576e399bd2ce3bd4df59" + "reference": "116ceba3c357f49d40854f3083f60faa372f5c25" }, "require": { "php": ">=7.2" @@ -2550,7 +2550,7 @@ "dist": { "type": "path", "url": "../../packages/scheduled-updates", - "reference": "5cedcdf836af8bf383629f3d0eeb6df48f33ff7d" + "reference": "c9decd746c2bb13c832778fceafceac8ed06fec8" }, "require": { "automattic/jetpack-connection": "@dev", @@ -3928,7 +3928,7 @@ "dist": { "type": "path", "url": "../../packages/phpunit-select-config", - "reference": "754e03c91b51aa261673505d13052e1fd754df21" + "reference": "f34d251d44e96b67a97272b464b69fc9ffb7de76" }, "require": { "composer-runtime-api": "^2.2.2", From 880441fb07851d01b60d6ba50bcd95002324c8ca Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:03:16 +1200 Subject: [PATCH 13/34] Premium Analytics: Stub isSimpleSite in the top-posts widget test The test replaces the whole jetpack-script-data module, so the data layer's new isSimpleSite import resolved to undefined and the stats query rejected before rendering rows. --- .../widgets/top-posts/__tests__/top-posts.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/packages/premium-analytics/widgets/top-posts/__tests__/top-posts.test.tsx b/projects/packages/premium-analytics/widgets/top-posts/__tests__/top-posts.test.tsx index 2640b4596ad8..d589f697926a 100644 --- a/projects/packages/premium-analytics/widgets/top-posts/__tests__/top-posts.test.tsx +++ b/projects/packages/premium-analytics/widgets/top-posts/__tests__/top-posts.test.tsx @@ -13,6 +13,7 @@ import TopPostsWidget from '../render'; jest.mock( '@automattic/jetpack-script-data', () => ( { getScriptData: jest.fn(), + isSimpleSite: jest.fn().mockReturnValue( false ), } ) ); jest.mock( '@wordpress/api-fetch', () => jest.fn() ); From 75301860aa056831c1fe015ce53ab5d9df128d9f Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:15:52 +1200 Subject: [PATCH 14/34] Premium Analytics: Route report fetchers through the stats transport Report calls were always stats-proxy v2 requests; the fetchers just built the path by hand. A fetchReport() helper on the shared resolver now owns the path, so no fetcher touches transport or environment concerns, and the reports path constants disappear. --- .../api/__tests__/stats-proxy-fetch.test.ts | 39 ++++++++++++++++++- .../packages/data/src/api/constants.ts | 10 +---- .../packages/data/src/api/index.ts | 1 + .../report-bookings-fetch.ts | 13 +------ .../report-conversion-rate-fetch.ts | 13 +------ .../report-coupons-by-date-fetch.ts | 11 +----- .../report-coupons-fetch.ts | 11 +----- .../report-customers-by-date-fetch.ts | 11 +----- .../report-customers-fetch.ts | 13 +------ ...port-order-attribution-by-product-fetch.ts | 13 ++----- .../report-order-attribution-summary-fetch.ts | 13 ++----- .../report-orders-fetch.ts | 18 +++------ .../report-products-fetch.ts | 11 +----- .../report-sessions-by-device-fetch.ts | 11 +----- .../report-visitors-by-location-fetch.ts | 11 +----- .../report-visitors-fetch.ts | 11 +----- .../data/src/api/stats-proxy-fetch.ts | 23 +++++++++-- .../stories/mocks/register-report-mocks.ts | 8 ++-- 18 files changed, 95 insertions(+), 146 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts index 8a7683c29eb9..b1a40c9373a9 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts @@ -5,7 +5,7 @@ import apiFetch from '@wordpress/api-fetch'; /** * Internal dependencies */ -import { fetchStatsProxy, getStatsProxyPath } from '../stats-proxy-fetch'; +import { fetchReport, fetchStatsProxy, getStatsProxyPath } from '../stats-proxy-fetch'; jest.mock( '@wordpress/api-fetch' ); @@ -164,6 +164,43 @@ describe( 'fetchStatsProxy', () => { } ); } ); + it( 'builds report paths through the local proxy by default', async () => { + await fetchReport( 'orders/by-date', { + from: '2026-06-01', + to: '2026-06-30', + interval: 'day', + } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/orders/by-date?from=2026-06-01&to=2026-06-30&interval=day', + method: 'GET', + } ); + } ); + + it( 'builds Simple report paths for the WPCOM apiFetch bridge', async () => { + setSimpleScriptData(); + + await fetchReport( 'sessions/by-device', { from: '2026-06-01', to: '2026-06-30' } ); + + expect( mockApiFetch ).toHaveBeenCalledWith( { + path: '/wpcom/v2/analytics/reports/sessions/by-device?from=2026-06-01&to=2026-06-30', + method: 'GET', + } ); + } ); + + it( 'serializes nested report filters into query args', async () => { + await fetchReport( 'coupons/by-date', { + from: '2026-06-01', + to: '2026-06-30', + filters: [ { key: 'product', operator: 'is', value: '42' } ], + } ); + + const path = ( mockApiFetch.mock.calls[ 0 ][ 0 ] as { path: string } ).path; + expect( decodeURIComponent( path ) ).toBe( + '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/coupons/by-date?from=2026-06-01&to=2026-06-30&filters[0][key]=product&filters[0][operator]=is&filters[0][value]=42' + ); + } ); + it( 'marks WPCOM Simple upgrades requests as global', async () => { setSimpleScriptData( 67890 ); diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts index 2e8e9e945ea8..872c8ddfd1a7 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/constants.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -7,18 +7,12 @@ import { isSimpleSite } from '@automattic/jetpack-script-data'; * Constants for API endpoints */ export const statsProxyPath = '/jetpack-premium-analytics/v1/proxy'; -export const reportsPath = `${ statsProxyPath }/v2/analytics/reports`; export const noticesPath = '/jetpack-premium-analytics/v1/notices'; -// WPCOM Simple has no local proxy or notices endpoint: requests use public-api -// namespaces directly, dispatched by WPCOM's wp-admin apiFetch bridge. -export const wpcomSimpleReportsPath = '/wpcom/v2/analytics/reports'; +// WPCOM Simple has no local notices endpoint: requests use the WPCOM Stats +// notices endpoint directly, dispatched by WPCOM's wp-admin apiFetch bridge. export const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; -export function getReportsPath() { - return isSimpleSite() ? wpcomSimpleReportsPath : reportsPath; -} - export function getNoticesPath() { return isSimpleSite() ? wpcomSimpleNoticesPath : noticesPath; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts index 7a206ec06b6b..08dae2352ba7 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -49,6 +49,7 @@ export type { ExportReportResponse, } from './report-export-fetch'; export { + fetchReport, fetchStatsProxy, getStatsProxyPath, type StatsProxyFetchParams, diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts index f011fee46cc9..d6456a217b84 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -44,15 +39,11 @@ export async function fetchReportBookings( { filters, date_type, }: RequestReportBookingsParams ): Promise< ReportsBookingsByDateResponse > { - const apiUrl = `${ getReportsPath() }/bookings/by-date`; - - const path = addQueryArgs( apiUrl, { + return fetchReport< ReportsBookingsByDateResponse >( 'bookings/by-date', { from, to, interval, filters, date_type, } ); - - return apiFetch( { path } ) as Promise< ReportsBookingsByDateResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts index 61a72e1e9b54..e7b5ee656d8b 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -45,14 +40,10 @@ export async function fetchReportConversionRate( { interval, filters, }: RequestReportConversionRateParams ): Promise< ReportsConversionRateByDateResponse > { - const path = addQueryArgs( `${ getReportsPath() }/sessions/by-conversion-rate`, { + return fetchReport< ReportsConversionRateByDateResponse >( 'sessions/by-conversion-rate', { from, to, interval, filters, } ); - - return apiFetch( { - path, - } ) as Promise< ReportsConversionRateByDateResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts index 93c648ec2022..2a257f67b353 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -55,13 +50,11 @@ export async function fetchReportCouponsByDate( { filters, date_type, }: RequestReportCouponsByDateParams ): Promise< ReportsCouponsByDateResponse > { - const path = addQueryArgs( `${ getReportsPath() }/coupons/by-date`, { + return fetchReport< ReportsCouponsByDateResponse >( 'coupons/by-date', { from, to, interval, filters, date_type, } ); - - return apiFetch< ReportsCouponsByDateResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts index e263758b724d..e5b8c2eba7c0 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -41,7 +36,7 @@ export async function fetchReportCoupons( { filters, date_type, }: RequestReportCouponsParams ): Promise< ReportsCouponsResponse > { - const path = addQueryArgs( `${ getReportsPath() }/coupons/`, { + return fetchReport< ReportsCouponsResponse >( 'coupons/', { from, to, interval, @@ -49,6 +44,4 @@ export async function fetchReportCoupons( { date_type, orderby: 'total_sales', } ); - - return apiFetch< ReportsCouponsResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts index bfade9b6ed60..6688c31b1fbc 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { BaseReportParams } from '../../utils/types'; type ReportsCustomersByDateSummary = { @@ -66,12 +61,10 @@ export async function fetchReportCustomersByDate( { interval, date_type, }: RequestReportCustomersByDateParams ): Promise< ReportsCustomersByDateResponse > { - const path = addQueryArgs( `${ getReportsPath() }/customers/by-date`, { + return fetchReport< ReportsCustomersByDateResponse >( 'customers/by-date', { from, to, interval, date_type, } ); - - return apiFetch( { path } ) as Promise< ReportsCustomersByDateResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts index 6cb6b0567182..709c724676dc 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -40,14 +35,10 @@ export async function fetchReportCustomers( { filters, date_type, }: RequestReportCustomersParams ): Promise< ReportsCustomersNewReturningResponse > { - const path = addQueryArgs( `${ getReportsPath() }/customers/new-returning`, { + return fetchReport< ReportsCustomersNewReturningResponse >( 'customers/new-returning', { from, to, filters, date_type, } ); - - return apiFetch( { - path, - } ) as Promise< ReportsCustomersNewReturningResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts index 67e8b3f25953..04fab8349236 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; import type { ORDER_ATTRIBUTION_VIEWS } from '../report-order-attribution-summary-fetch/report-order-attribution-summary-fetch'; @@ -67,10 +62,8 @@ export async function fetchReportOrderAttributionByProduct( queryParams.filters = filters; } - const path = addQueryArgs( - `${ getReportsPath() }/order-attribution-by-product/${ view }/summary`, + return fetchReport< OrderAttributionByProductResponse >( + `order-attribution-by-product/${ view }/summary`, queryParams ); - - return apiFetch< OrderAttributionByProductResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts index d52ee9e58c3f..727609b4cd85 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { BaseReportParams } from '../../utils/types'; export const ORDER_ATTRIBUTION_VIEWS = [ @@ -79,10 +74,8 @@ export async function fetchReportOrderAttributionSummary( date_type, }; - const path = addQueryArgs( - `${ getReportsPath() }/order-attribution/${ view }/summary`, + return fetchReport< OrderAttributionSummaryResponse >( + `order-attribution/${ view }/summary`, queryParams ); - - return apiFetch< OrderAttributionSummaryResponse >( { path } ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts index 66c130b60b43..5ed9e042f1c6 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts @@ -1,13 +1,8 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ import { hasProductFilters } from '../../utils/product-filters'; -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; import type { BaseReportParams } from '../../utils/types'; @@ -51,18 +46,15 @@ export async function fetchReportOrders( { filters, date_type, }: RequestReportOrdersParams ): Promise< ReportsOrdersByDateResponse > { - const hasProductFiltersValue = hasProductFilters( filters ); - const apiUrl = hasProductFiltersValue - ? `${ getReportsPath() }/orders-by-product-type/by-date` - : `${ getReportsPath() }/orders/by-date`; + const endpoint = hasProductFilters( filters ) + ? 'orders-by-product-type/by-date' + : 'orders/by-date'; - const path = addQueryArgs( apiUrl, { + return fetchReport< ReportsOrdersByDateResponse >( endpoint, { from, to, interval, filters, date_type, } ); - - return apiFetch( { path } ) as Promise< ReportsOrdersByDateResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts index 91b0c332abd2..614d4eacf99a 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -1,13 +1,8 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ import { BaseReportParams } from '../../utils/types'; -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { FilterCondition } from '../../types/filter-condition'; export type RequestReportProductsParams = Omit< BaseReportParams, 'interval' > & { @@ -66,7 +61,5 @@ export async function fetchReportProducts( queryArgs.filters = params.filters; } - return apiFetch< ReportProductsResponse >( { - path: addQueryArgs( `${ getReportsPath() }/products`, queryArgs ), - } ); + return fetchReport< ReportProductsResponse >( 'products', queryArgs ); } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts index 4f93fb5f40f8..5300027b05f9 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { BaseReportParams } from '../../utils/types'; /** @@ -51,10 +46,8 @@ export async function fetchReportSessionsByDevice( { from, to, }: RequestReportSessionsByDeviceParams ): Promise< ReportsSessionsByDeviceResponse > { - const path = addQueryArgs( `${ getReportsPath() }/sessions/by-device`, { + return fetchReport< ReportsSessionsByDeviceResponse >( 'sessions/by-device', { from, to, } ); - - return apiFetch( { path } ) as Promise< ReportsSessionsByDeviceResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts index f71ee2b4abc8..de2af1014ad2 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { BaseReportParams } from '../../utils/types'; type VisitorsByLocationReportDataItem = { @@ -47,7 +42,7 @@ export async function fetchReportVisitorsByLocation( { country_code, limit, }: RequestReportVisitorsByLocationParams ): Promise< ReportsVisitorsByLocationResponse > { - const path = addQueryArgs( `${ getReportsPath() }/sessions/by-location`, { + return fetchReport< ReportsVisitorsByLocationResponse >( 'sessions/by-location', { from, to, interval, @@ -55,6 +50,4 @@ export async function fetchReportVisitorsByLocation( { country_code, limit, } ); - - return apiFetch( { path } ) as Promise< ReportsVisitorsByLocationResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts index 85fd8cbc25ed..4f567024cbb4 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts @@ -1,12 +1,7 @@ -/** - * External dependencies - */ -import apiFetch from '@wordpress/api-fetch'; -import { addQueryArgs } from '@wordpress/url'; /** * Internal dependencies */ -import { getReportsPath } from '../constants'; +import { fetchReport } from '../stats-proxy-fetch'; import type { BaseReportParams } from '../../utils/types'; type ReportsVisitorsByDateSummary = { @@ -32,11 +27,9 @@ export async function fetchReportVisitors( { to, interval, }: RequestReportVisitorsParams ): Promise< ReportsVisitorsByDateResponse > { - const path = addQueryArgs( `${ getReportsPath() }/sessions/by-date`, { + return fetchReport< ReportsVisitorsByDateResponse >( 'sessions/by-date', { from, to, interval, } ); - - return apiFetch( { path } ) as Promise< ReportsVisitorsByDateResponse >; } diff --git a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts index 28309d140943..7c433fb903c3 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts @@ -13,10 +13,7 @@ export type StatsProxyVersion = '1.1' | '1.2' | '2'; export type StatsProxyMethod = 'GET' | 'POST'; -export type StatsProxyParams = Record< - string, - string | number | boolean | undefined | Array< string | number | boolean > ->; +export type StatsProxyParams = Record< string, unknown >; export type StatsProxyFetchParams< TBody = unknown > = { version: StatsProxyVersion; @@ -100,6 +97,24 @@ export function getStatsProxyPath( return resolveStatsProxyRequest( request ).path; } +/** + * Fetch a Woo analytics report through the stats transport. + * + * @param endpoint - Report endpoint below `analytics/reports`, e.g. `orders/by-date`. + * @param params - Query params of the report request. + * @return The report response. + */ +export async function fetchReport< TResponse = unknown >( + endpoint: string, + params?: StatsProxyParams +): Promise< TResponse > { + return fetchStatsProxy< TResponse >( { + version: '2', + endpoint: `analytics/reports/${ normalizeEndpoint( endpoint ) }`, + params, + } ); +} + export async function fetchStatsProxy< TResponse = unknown, TBody = unknown >( { version, endpoint, diff --git a/projects/packages/premium-analytics/packages/widgets-toolkit/src/stories/mocks/register-report-mocks.ts b/projects/packages/premium-analytics/packages/widgets-toolkit/src/stories/mocks/register-report-mocks.ts index c38322803dcc..60849a84806d 100644 --- a/projects/packages/premium-analytics/packages/widgets-toolkit/src/stories/mocks/register-report-mocks.ts +++ b/projects/packages/premium-analytics/packages/widgets-toolkit/src/stories/mocks/register-report-mocks.ts @@ -7,8 +7,8 @@ * * To mock report data we register an `apiFetch` middleware that intercepts the * proxy report paths and returns generated mock data. The data package fetches - * every report through `apiFetch( { path } )` using the same base path - * (`reportsPath`), so a single middleware covers all widget stories. + * every report through `fetchReport()`, which builds the same base path, so a + * single middleware covers all widget stories. * * The middleware is registered exactly once (guarded by a module-level flag) and * is triggered automatically when `with-widget-root.tsx` is imported. @@ -73,8 +73,8 @@ import { getMockParamsFromPreset } from './presets'; import type { APIFetchMiddleware, APIFetchOptions } from '@wordpress/api-fetch'; /** - * Base path for Woo analytics report requests. Matches `reportsPath` in the data - * package (`@jetpack-premium-analytics/data`). + * Base path for Woo analytics report requests. Matches the non-Simple path + * built by `fetchReport()` in the data package (`@jetpack-premium-analytics/data`). */ const API_BASE = '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports'; const STATS_FOLLOWERS_PATH = '/jetpack-premium-analytics/v1/proxy/v1.1/stats/followers'; From 64c372acb517856427086483006ff21df3f25f7f Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:26:21 +1200 Subject: [PATCH 15/34] Premium Analytics: Skip dashboard route registration on Simple Simple's dashboard requests go to WPCOM public-api, where the support endpoints are registered separately. The rest_api_init hooks move from file scope into boot(), so the Simple bootstrap registers no local REST surface while connected Jetpack sites keep serving the routes locally. --- .../premium-analytics/src/class-analytics.php | 23 +++++++++++----- .../src/dashboard-layout.php | 1 - .../src/dashboard-sections.php | 1 - .../premium-analytics/src/widget-modules.php | 1 - .../tests/php/Analytics_Test.php | 26 +++++++++++++++++++ 5 files changed, 42 insertions(+), 10 deletions(-) diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 58c6a7fdaeb6..2a82308a9ce7 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -54,8 +54,9 @@ public static function init( $options = array() ) { * Initialize the Analytics app on WordPress.com Simple. * * Simple serves the dashboard from WPCOM and uses WPCOM's wp-admin apiFetch bridge to reach - * public-api.wordpress.com directly, so it does not register the local proxy, notices, or sync - * bootstrap surfaces used by connected Jetpack sites. + * public-api.wordpress.com directly, so it registers none of the site-served REST surfaces + * used by connected Jetpack sites: the proxy, notices, sync bootstrap, and the dashboard + * support routes, which WPCOM registers separately on public-api. * * @param array $options Optional configuration options. * Supported keys: @@ -124,17 +125,25 @@ private static function boot( $options, $register_local_services ) { // Hydrate the registry with the availability filter in place. bootstrap_widget_types(); - // Expose dashboard widget modules over REST and wire them into the - // page import map for dynamic import() on the client. + // Wire dashboard widget modules into the page import map for dynamic + // import() on the client. require_once __DIR__ . '/widget-modules.php'; - // Register the dashboard's default layout: the first-load preference - // injection and the REST route the "reset to default" action reads. + // Register the dashboard's default layout first-load preference injection. require_once __DIR__ . '/dashboard-layout.php'; - // Register dashboard sections and expose section metadata/defaults over REST. + // Register dashboard sections and their default layout seeding. require_once __DIR__ . '/dashboard-sections.php'; + // Dashboard support routes (widget modules, default layout, sections). + // Simple is served by WPCOM public-api, where these endpoints are + // registered separately; only connected Jetpack sites serve them locally. + if ( $register_local_services ) { + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); + } + // Expose opt-in CSV export settings to the dashboard. require_once __DIR__ . '/csv-exports.php'; configure_csv_exports(); diff --git a/projects/packages/premium-analytics/src/dashboard-layout.php b/projects/packages/premium-analytics/src/dashboard-layout.php index d0bf1e77934a..b94ff7c49e1e 100644 --- a/projects/packages/premium-analytics/src/dashboard-layout.php +++ b/projects/packages/premium-analytics/src/dashboard-layout.php @@ -200,7 +200,6 @@ function register_dashboard_default_layout_route() { ) ); } -add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); /** * Builds a widget instance for bundled dashboard defaults. diff --git a/projects/packages/premium-analytics/src/dashboard-sections.php b/projects/packages/premium-analytics/src/dashboard-sections.php index 4e415232ef4d..594aaf73583a 100644 --- a/projects/packages/premium-analytics/src/dashboard-sections.php +++ b/projects/packages/premium-analytics/src/dashboard-sections.php @@ -651,4 +651,3 @@ function register_dashboard_sections_rest_routes() { } bootstrap_dashboard_sections(); -add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); diff --git a/projects/packages/premium-analytics/src/widget-modules.php b/projects/packages/premium-analytics/src/widget-modules.php index 0e170aac63e4..0aee0d7c827e 100644 --- a/projects/packages/premium-analytics/src/widget-modules.php +++ b/projects/packages/premium-analytics/src/widget-modules.php @@ -32,7 +32,6 @@ function register_widget_modules_rest_route() { ) ); } -add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); /** * Build the REST response: one record per available widget type. diff --git a/projects/packages/premium-analytics/tests/php/Analytics_Test.php b/projects/packages/premium-analytics/tests/php/Analytics_Test.php index 5326885a0534..ae24f41ced8d 100644 --- a/projects/packages/premium-analytics/tests/php/Analytics_Test.php +++ b/projects/packages/premium-analytics/tests/php/Analytics_Test.php @@ -76,6 +76,32 @@ public function test_init_skips_local_proxy_in_wpcom_simple_mode() { $this->assertFalse( has_filter( 'jetpack_stats_transient_cleanup_prefixes' ) ); } + /** + * Normal package bootstrap serves the dashboard support routes from the site. + */ + public function test_init_registers_dashboard_support_routes_by_default() { + $this->reset_analytics_init_state(); + + Analytics::init(); + + $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ) ); + $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ) ); + $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ) ); + } + + /** + * WordPress.com Simple mode leaves the dashboard support routes to public-api. + */ + public function test_init_skips_dashboard_support_routes_in_wpcom_simple_mode() { + $this->reset_analytics_init_state(); + + Analytics::init_wpcom_simple(); + + $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ) ); + $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ) ); + $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ) ); + } + /** * The full-page dashboard slug in admin is recognized as a dashboard request. */ From 0bb3028f00771fefe16a25b57eb08b8074692bd1 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:30:11 +1200 Subject: [PATCH 16/34] Premium Analytics: Compose init entry points from atomic boot steps Replace the shared boot() and its boolean flag with named private steps. init() and init_wpcom_simple() each read as an explicit list of what their platform registers, so the Simple/Jetpack difference is visible at the entry point instead of buried in flag branches. --- .../premium-analytics/src/class-analytics.php | 203 +++++++++++++----- 1 file changed, 146 insertions(+), 57 deletions(-) diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 2a82308a9ce7..665448f604f0 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -39,7 +39,10 @@ class Analytics { private static $menu_title = 'Analytics'; /** - * Initialize the Analytics app. + * Initialize the Analytics app on a connected Jetpack site. + * + * Registers the full local surface: the site serves the WPCOM data proxy, + * notices, sync bootstrap, and the dashboard support routes itself. * * @param array $options Optional configuration options. * Supported keys: @@ -47,16 +50,32 @@ class Analytics { * @return void */ public static function init( $options = array() ) { - self::boot( $options, true ); + if ( self::$initialized ) { + return; + } + self::$initialized = true; + self::apply_options( $options ); + + self::register_sync_bootstrap(); + self::register_local_api(); + self::register_stats_tracking(); + self::register_store_event_tracking(); + self::register_report_csv_export(); + self::load_widget_registry(); + self::load_dashboard_components(); + self::register_dashboard_support_routes(); + self::load_build(); + self::register_admin_page(); } /** * Initialize the Analytics app on WordPress.com Simple. * - * Simple serves the dashboard from WPCOM and uses WPCOM's wp-admin apiFetch bridge to reach - * public-api.wordpress.com directly, so it registers none of the site-served REST surfaces - * used by connected Jetpack sites: the proxy, notices, sync bootstrap, and the dashboard - * support routes, which WPCOM registers separately on public-api. + * Simple serves the dashboard from WPCOM and uses WPCOM's wp-admin apiFetch + * bridge to reach public-api.wordpress.com directly, so it registers none of + * the site-served REST surfaces a connected Jetpack site provides: no proxy, + * no notices, no sync bootstrap, and no dashboard support routes — WPCOM + * registers those endpoints separately on public-api. * * @param array $options Optional configuration options. * Supported keys: @@ -64,56 +83,100 @@ public static function init( $options = array() ) { * @return void */ public static function init_wpcom_simple( $options = array() ) { - self::boot( $options, false ); + if ( self::$initialized ) { + return; + } + self::$initialized = true; + self::apply_options( $options ); + + self::register_store_event_tracking(); + self::register_report_csv_export(); + self::load_widget_registry(); + self::load_dashboard_components(); + self::load_build(); + self::register_admin_page(); } /** - * Shared package bootstrap. + * Apply init-time configuration options. * - * @param array $options Optional configuration options. - * @param bool $register_local_services Whether to register connected-site local services. + * @param array $options Options passed to the init entry points. * @return void */ - private static function boot( $options, $register_local_services ) { - if ( self::$initialized ) { - return; - } - - self::$initialized = true; - + private static function apply_options( $options ) { if ( ! empty( $options['menu_title'] ) ) { self::$menu_title = $options['menu_title']; } + } - // Connected-site services self-gate on their own hooks; Simple reaches - // WPCOM APIs directly through the WPCOM wp-admin apiFetch bridge. - if ( $register_local_services ) { - // Keep the shared connection available when another connection-owning plugin is deactivated. - Connection_Configuration::configure(); - - // Always on: sync runs in cron; REST routes + registry serve REST requests - // (is_admin() false). REST_REQUEST isn't defined this early, so they - // self-gate on their own rest_api_init / init hooks. - Sync_Status_Tracker::configure(); - - // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so - // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands. - Sync_Configuration::register(); - Api_Proxy_Controller::register(); - Notices_Controller::register(); - - // Piggybacks on the Jetpack Stats module; checks Jetpack connection state. - Jetpack_Stats_Tracker::configure(); - } + /** + * Register the sync services that feed the local data pipeline. + * + * @return void + */ + private static function register_sync_bootstrap() { + // Keep the shared connection available when another connection-owning plugin is deactivated. + Connection_Configuration::configure(); + + Sync_Status_Tracker::configure(); + + // TEMPORARY (WOOA7S-1550): register the interim woocommerce_analytics sync module so + // Sync_Status_Tracker has a full sync to observe. Remove when the shared sync-modules package lands. + Sync_Configuration::register(); + } + + /** + * Configure the Jetpack Stats front-end tracking module. + * + * Piggybacks on the Jetpack Stats module and checks Jetpack connection + * state, so connected sites only. + * + * @return void + */ + private static function register_stats_tracking() { + Jetpack_Stats_Tracker::configure(); + } + + /** + * Register the site-served REST API: the WPCOM data proxy and notices. + * + * Both self-gate on their own rest_api_init hooks. + * + * @return void + */ + private static function register_local_api() { + Api_Proxy_Controller::register(); + Notices_Controller::register(); + } - // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). + /** + * Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). + * + * @return void + */ + private static function register_store_event_tracking() { WooCommerce_Analytics_Tracker::configure(); + } - // CSV report export pipeline (WOOA7S-1581). Must register above the is_admin() gate so its - // REST route hooks rest_api_init (is_admin() is false during REST requests). Self-gates on - // WooCommerce being active + Jetpack connected. + /** + * Register the CSV report export pipeline (WOOA7S-1581). + * + * Must register above the is_admin() gate so its REST route hooks + * rest_api_init (is_admin() is false during REST requests). Self-gates on + * WooCommerce being active + Jetpack connected. + * + * @return void + */ + private static function register_report_csv_export() { Export::configure(); + } + /** + * Load and hydrate the widget type registry. + * + * @return void + */ + private static function load_widget_registry() { // Load the widget type registry: hydration routine, registry-time and // runtime filters, and the registry accessors. require_once __DIR__ . '/widget-types.php'; @@ -124,7 +187,14 @@ private static function boot( $options, $register_local_services ) { // Hydrate the registry with the availability filter in place. bootstrap_widget_types(); + } + /** + * Load the dashboard components every platform renders with. + * + * @return void + */ + private static function load_dashboard_components() { // Wire dashboard widget modules into the page import map for dynamic // import() on the client. require_once __DIR__ . '/widget-modules.php'; @@ -135,31 +205,50 @@ private static function boot( $options, $register_local_services ) { // Register dashboard sections and their default layout seeding. require_once __DIR__ . '/dashboard-sections.php'; - // Dashboard support routes (widget modules, default layout, sections). - // Simple is served by WPCOM public-api, where these endpoints are - // registered separately; only connected Jetpack sites serve them locally. - if ( $register_local_services ) { - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); - } - // Expose opt-in CSV export settings to the dashboard. require_once __DIR__ . '/csv-exports.php'; configure_csv_exports(); + } + + /** + * Serve the dashboard support routes (widget modules, default layout, + * sections) from the site. + * + * Simple skips this: WPCOM registers these endpoints separately on + * public-api, and Simple dashboard requests are dispatched there. + * + * @return void + */ + private static function register_dashboard_support_routes() { + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); + add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); + } - // Load wp-build output (interceptor, modules, routes, page render). - // Must stay above the is_admin() gate: build/widgets.php defines the - // manifest the widget registry reads, and the registry serves REST - // requests (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render - // pieces here self-gate on admin_init, so loading them globally is inert - // off the dashboard. Only the polyfill registration below is admin-scoped. + /** + * Load the wp-build output (interceptor, modules, routes, page render). + * + * Must run before the is_admin() gate: build/widgets.php defines the + * manifest the widget registry reads, and the registry serves REST requests + * (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render + * pieces here self-gate on admin_init, so loading them globally is inert + * off the dashboard. + * + * @return void + */ + private static function load_build() { $build_entry = __DIR__ . '/../build/build.php'; if ( file_exists( $build_entry ) ) { require_once $build_entry; } + } - // Below: admin-only render path (assets, menu). + /** + * Register the admin-only render path: polyfills, menu, and page hooks. + * + * @return void + */ + private static function register_admin_page() { if ( ! is_admin() ) { return; } From 067ebcc8811c2c590bb3a6177e05d513259a42eb Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:38:20 +1200 Subject: [PATCH 17/34] Revert "Premium Analytics: Drop composer versions overrides" This reverts commit 81290b04a1cfe394d2091f6d1754efe4f9dde24f. --- .../packages/jetpack-mu-wpcom/composer.json | 9 ++- .../packages/premium-analytics/composer.json | 9 ++- .../plugins/mu-wpcom-plugin/composer.lock | 4 +- .../plugins/premium-analytics/composer.lock | 2 +- projects/plugins/wpcomsh/composer.json | 9 ++- projects/plugins/wpcomsh/composer.lock | 78 +++++++++---------- 6 files changed, 66 insertions(+), 45 deletions(-) diff --git a/projects/packages/jetpack-mu-wpcom/composer.json b/projects/packages/jetpack-mu-wpcom/composer.json index 0ffd78c77ed2..4b0372445253 100644 --- a/projects/packages/jetpack-mu-wpcom/composer.json +++ b/projects/packages/jetpack-mu-wpcom/composer.json @@ -64,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" + } } } ], diff --git a/projects/packages/premium-analytics/composer.json b/projects/packages/premium-analytics/composer.json index 020a7855c061..e5dccf9c5dcf 100644 --- a/projects/packages/premium-analytics/composer.json +++ b/projects/packages/premium-analytics/composer.json @@ -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" + } } } ], diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index 403670dbe6e5..41e6b84f1bfe 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1371,7 +1371,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" + "reference": "f5a0802d45daac83c0bf9df90cf6f330527227cf" }, "require": { "automattic/block-delimiter": "@dev", @@ -1727,7 +1727,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" + "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" }, "require": { "automattic/jetpack-assets": "@dev", diff --git a/projects/plugins/premium-analytics/composer.lock b/projects/plugins/premium-analytics/composer.lock index fc065d9d84c0..ad94511f0030 100644 --- a/projects/plugins/premium-analytics/composer.lock +++ b/projects/plugins/premium-analytics/composer.lock @@ -841,7 +841,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" + "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" }, "require": { "automattic/jetpack-assets": "@dev", diff --git a/projects/plugins/wpcomsh/composer.json b/projects/plugins/wpcomsh/composer.json index b5113604c94b..4df53e7f4412 100644 --- a/projects/plugins/wpcomsh/composer.json +++ b/projects/plugins/wpcomsh/composer.json @@ -62,7 +62,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" + } } }, { diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index 622f4871f491..dc596b40ab29 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "d0871f7a7395d1ee5864c7d40d2e35df", + "content-hash": "c9d4bc712b476604df193f64608b93ab", "packages": [ { "name": "automattic/block-delimiter", @@ -12,7 +12,7 @@ "dist": { "type": "path", "url": "../../packages/block-delimiter", - "reference": "e860d7feb6347341faad763d7e342cc6a5e8b449" + "reference": "bb5cd334fb6d00ed5d5c26f6f7cff9b3ae55ae99" }, "require": { "php": ">=7.2" @@ -123,7 +123,7 @@ "dist": { "type": "path", "url": "../../packages/a8c-mc-stats", - "reference": "48895090b425c307b50fdf77f2c770719e1d3cf4" + "reference": "003b710eb1eaf27ed970000b41a292c89fcfe5ba" }, "require": { "php": ">=7.2" @@ -176,7 +176,7 @@ "dist": { "type": "path", "url": "../../packages/admin-ui", - "reference": "96398da45af93491fdd90cea96331e624b42e39e" + "reference": "edc5a654cdffde980eafb17c2c1d98ca755957ad" }, "require": { "automattic/jetpack-redirect": "@dev", @@ -253,7 +253,7 @@ "dist": { "type": "path", "url": "../../packages/agents-manager", - "reference": "b4b58a385a26210a56620238aee1779e9373d195" + "reference": "7a2c9058e35e94a42562eee55d0528376601801b" }, "require": { "automattic/jetpack-assets": "@dev", @@ -328,7 +328,7 @@ "dist": { "type": "path", "url": "../../packages/assets", - "reference": "7214a03808294ae652164b2495dc4e8a09c54e89" + "reference": "5b5ad5da3b83d4a98780a175d17df9fe61e58350" }, "require": { "automattic/jetpack-constants": "@dev", @@ -401,7 +401,7 @@ "dist": { "type": "path", "url": "../../packages/autoloader", - "reference": "afee041b24440e62c6ddb502b3c6716044f8c3ed" + "reference": "e38ca7a8dc245675a896e6f1e126bdf6b5157ba6" }, "require": { "composer-plugin-api": "^2.2", @@ -469,7 +469,7 @@ "dist": { "type": "path", "url": "../../packages/blaze", - "reference": "66186f7bc5595357512833c76816e8496b6a6c0b" + "reference": "6f33631a30fe59d482659c8ce80da39e349a0e83" }, "require": { "automattic/jetpack-admin-ui": "@dev", @@ -545,7 +545,7 @@ "dist": { "type": "path", "url": "../../packages/blocks", - "reference": "21984233a8e0e6f3dd700bdaa35901f0f9c3162f" + "reference": "22e2fd3dc418c457cd5113b9cc4e040966ec2128" }, "require": { "automattic/jetpack-constants": "@dev", @@ -601,7 +601,7 @@ "dist": { "type": "path", "url": "../../packages/classic-theme-helper", - "reference": "de10e684e8c1cc0bdb66910f27aae22bdcfce75f" + "reference": "94619701fa1a58360a0a7e0afe159bc2dd7e5095" }, "require": { "automattic/jetpack-assets": "@dev", @@ -667,7 +667,7 @@ "dist": { "type": "path", "url": "../../packages/compat", - "reference": "cca3c26e1aa14e6cfe16ed55099dd1fc39362778" + "reference": "08939574a981fd4dc944070d7a3948decd9ff37a" }, "require": { "php": ">=7.2" @@ -701,7 +701,7 @@ "dist": { "type": "path", "url": "../../packages/composer-plugin", - "reference": "4c4b86f8b4a330c7033d2f49e33397d044a3d7a0" + "reference": "15799e1372ef4a4b2346310027df6bcef1c1ab87" }, "require": { "composer-plugin-api": "^2.2", @@ -761,7 +761,7 @@ "dist": { "type": "path", "url": "../../packages/config", - "reference": "c57552fadeaa0ee131577eea652128b3686f62cc" + "reference": "639ed7b8e15b027b74ede1651392cde0bf4928f4" }, "require": { "php": ">=7.2" @@ -832,7 +832,7 @@ "dist": { "type": "path", "url": "../../packages/connection", - "reference": "ce362d9d8bf97228d5267f8d4a9b05379ff98aa2" + "reference": "ac92fb106fb48e2bdfe0946da6e387b2f61585fb" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -920,7 +920,7 @@ "dist": { "type": "path", "url": "../../packages/constants", - "reference": "a2f68c778c6c47b3e9f7699379fe5efa8ffe2acc" + "reference": "7cc5b2f7d1f65f32d8ee5c1d11b6df716fcb6db1" }, "require": { "php": ">=7.2" @@ -974,7 +974,7 @@ "dist": { "type": "path", "url": "../../packages/device-detection", - "reference": "060e4e98b1fb7351b6635c5c8c16a0a2f365ce90" + "reference": "9ab4dcf177ba5b09afe97c13f6f538edf7ba5fd8" }, "require": { "php": ">=7.2" @@ -1095,7 +1095,7 @@ "dist": { "type": "path", "url": "../../packages/google-analytics", - "reference": "93748801f1647c1a443f8af82f79eac985303dfa" + "reference": "c03943f744703f6239ee5cc78fd5c29f2bc8de37" }, "require": { "automattic/jetpack-status": "@dev", @@ -1160,7 +1160,7 @@ "dist": { "type": "path", "url": "../../packages/image-cdn", - "reference": "9e71f33ab5611baafd105ddf7200e13be88fe827" + "reference": "ff73ec5136b5760ef6f0dc5dc7296a7a2fa70ae8" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1220,7 +1220,7 @@ "dist": { "type": "path", "url": "../../packages/ip", - "reference": "d63dccb293846fdd6ef54fb78848d043815de4bb" + "reference": "e487e4a8e010cf98cac25b18e858529de439dcb9" }, "require": { "php": ">=7.2" @@ -1278,7 +1278,7 @@ "dist": { "type": "path", "url": "../../packages/jitm", - "reference": "9060a71b2749d8026a07f334efc6e536114a7ced" + "reference": "c32c102004bd39788ef69c33756e132659506751" }, "require": { "automattic/jetpack-a8c-mc-stats": "@dev", @@ -1353,7 +1353,7 @@ "dist": { "type": "path", "url": "../../packages/logo", - "reference": "9772ff10ed4f2e82abda216a2631b547d0a562e9" + "reference": "8e9b400315ba6fe6e556332c20310fb635f16185" }, "require": { "php": ">=7.2" @@ -1406,7 +1406,7 @@ "dist": { "type": "path", "url": "../../packages/masterbar", - "reference": "2c9c34203668df2e980fdf37d8f871bac19b11c9" + "reference": "09ed33eb6a354978d8e5e1d41bfa737d7a329e40" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1487,7 +1487,7 @@ "dist": { "type": "path", "url": "../../packages/jetpack-mu-wpcom", - "reference": "0839906c853365ec67456e3cdc6e6fa4fa50b5f5" + "reference": "ce82f8a3e5609f987876b4af12a00e797b7ef709" }, "require": { "automattic/block-delimiter": "@dev", @@ -1583,7 +1583,7 @@ "dist": { "type": "path", "url": "../../packages/password-checker", - "reference": "d9e7a7a8db710ebc8d064ae207775df8bd9e6662" + "reference": "8dd32a3ef5322b35a7caaaace68bd695605075cc" }, "require": { "php": ">=7.2" @@ -1638,7 +1638,7 @@ "dist": { "type": "path", "url": "../../packages/plans", - "reference": "d694876b1987293d1f9c0de3ff6ae37d8be95e85" + "reference": "a6e564eabb06e64767288117ae86db0432ec1099" }, "require": { "automattic/jetpack-connection": "@dev", @@ -1776,7 +1776,7 @@ "dist": { "type": "path", "url": "../../packages/post-list", - "reference": "1e835cbf7e005405652bee4c2f9460979de1bf92" + "reference": "1bf8d892fc7ff9671e0fa02d9e2101b707c6eee0" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1841,7 +1841,7 @@ "dist": { "type": "path", "url": "../../packages/post-media", - "reference": "e1c691c5b9ca6debdbf8351aaf6658189f1c0aa6" + "reference": "693f3207f6937211e1761f9d05b2864a74ff38ac" }, "require": { "automattic/block-delimiter": "@dev", @@ -1908,7 +1908,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "2520bcc0d3875df8436b0fbef27888e00700b989" + "reference": "b3511e8ca26dd4052147f368ebf1b0aa9179a787" }, "require": { "automattic/jetpack-assets": "@dev", @@ -1980,7 +1980,7 @@ "dist": { "type": "path", "url": "../../packages/redirect", - "reference": "84a8949d203512d279ad65280af8d2af01d4d799" + "reference": "654ba6e8dda4889250e4e671134aeb0bc8755481" }, "require": { "automattic/jetpack-status": "@dev", @@ -2035,7 +2035,7 @@ "dist": { "type": "path", "url": "../../packages/roles", - "reference": "a9e558a67db231ecf3194a02125a4b3f14a48b94" + "reference": "df8247a8970fdcc53b27916231a1ed5482f270f2" }, "require": { "php": ">=7.2" @@ -2089,7 +2089,7 @@ "dist": { "type": "path", "url": "../../packages/rtc", - "reference": "cf9257a7cb581975e66e8cee0e5dc04d3d4da7a1" + "reference": "62dcc023afd4f82da7ad6145e797122ab63091ad" }, "require": { "automattic/jetpack-assets": "@dev", @@ -2155,7 +2155,7 @@ "dist": { "type": "path", "url": "../../packages/stats", - "reference": "353dcc22a8de746e21bd2c6334d1ca7206fea704" + "reference": "e842f3502fce83a510a8115036d51edd9c5b1274" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2222,7 +2222,7 @@ "dist": { "type": "path", "url": "../../packages/stats-admin", - "reference": "69020479c6220930ee90793a30b40ecade9de84d" + "reference": "aa4d6effbd21f16943f4f5d4b80412c8711efc11" }, "require": { "automattic/jetpack-blaze": "@dev", @@ -2290,7 +2290,7 @@ "dist": { "type": "path", "url": "../../packages/status", - "reference": "30e50be852b4f6b22a2a47ed7ab3ef6daa4074b6" + "reference": "1d6ff66fe1a7869d8acef38cc32289aa3dbdd7c6" }, "require": { "automattic/jetpack-constants": "@dev", @@ -2354,7 +2354,7 @@ "dist": { "type": "path", "url": "../../packages/sync", - "reference": "0e6c312d73cf8b2bde57a5656e16e42c1b593217" + "reference": "6b066f38821fc99458a762fe0626c2b03c2ed819" }, "require": { "automattic/jetpack-connection": "@dev", @@ -2426,7 +2426,7 @@ "dist": { "type": "path", "url": "../../packages/wp-abilities", - "reference": "97a1a9548ae11e0cd0f0514d81c0701474968828" + "reference": "7af51269db986f7bde09d44d66b497c0709e8b74" }, "require": { "php": ">=7.2" @@ -2484,7 +2484,7 @@ "dist": { "type": "path", "url": "../../packages/wp-build-polyfills", - "reference": "116ceba3c357f49d40854f3083f60faa372f5c25" + "reference": "552586a6a5288c977124576e399bd2ce3bd4df59" }, "require": { "php": ">=7.2" @@ -2550,7 +2550,7 @@ "dist": { "type": "path", "url": "../../packages/scheduled-updates", - "reference": "c9decd746c2bb13c832778fceafceac8ed06fec8" + "reference": "5cedcdf836af8bf383629f3d0eeb6df48f33ff7d" }, "require": { "automattic/jetpack-connection": "@dev", @@ -3928,7 +3928,7 @@ "dist": { "type": "path", "url": "../../packages/phpunit-select-config", - "reference": "f34d251d44e96b67a97272b464b69fc9ffb7de76" + "reference": "754e03c91b51aa261673505d13052e1fd754df21" }, "require": { "composer-runtime-api": "^2.2.2", From f40506b06a441445caa75bf6bf664a26acb8a34e Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:42:11 +1200 Subject: [PATCH 18/34] Premium Analytics: Inline single-call boot steps One-line wrapper methods hide nothing; call the collaborators directly from the entry points and keep named steps only for multi-line pieces. --- .../premium-analytics/src/class-analytics.php | 57 ++++++------------- 1 file changed, 18 insertions(+), 39 deletions(-) diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 665448f604f0..3ffdee7ee2a3 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -58,9 +58,17 @@ public static function init( $options = array() ) { self::register_sync_bootstrap(); self::register_local_api(); - self::register_stats_tracking(); - self::register_store_event_tracking(); - self::register_report_csv_export(); + + // Piggybacks on the Jetpack Stats module; checks Jetpack connection state. + Jetpack_Stats_Tracker::configure(); + + // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). + WooCommerce_Analytics_Tracker::configure(); + + // 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::load_widget_registry(); self::load_dashboard_components(); self::register_dashboard_support_routes(); @@ -89,8 +97,13 @@ public static function init_wpcom_simple( $options = array() ) { self::$initialized = true; self::apply_options( $options ); - self::register_store_event_tracking(); - self::register_report_csv_export(); + // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). + WooCommerce_Analytics_Tracker::configure(); + + // 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::load_widget_registry(); self::load_dashboard_components(); self::load_build(); @@ -125,18 +138,6 @@ private static function register_sync_bootstrap() { Sync_Configuration::register(); } - /** - * Configure the Jetpack Stats front-end tracking module. - * - * Piggybacks on the Jetpack Stats module and checks Jetpack connection - * state, so connected sites only. - * - * @return void - */ - private static function register_stats_tracking() { - Jetpack_Stats_Tracker::configure(); - } - /** * Register the site-served REST API: the WPCOM data proxy and notices. * @@ -149,28 +150,6 @@ private static function register_local_api() { Notices_Controller::register(); } - /** - * Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). - * - * @return void - */ - private static function register_store_event_tracking() { - WooCommerce_Analytics_Tracker::configure(); - } - - /** - * Register the CSV report export pipeline (WOOA7S-1581). - * - * Must register above the is_admin() gate so its REST route hooks - * rest_api_init (is_admin() is false during REST requests). Self-gates on - * WooCommerce being active + Jetpack connected. - * - * @return void - */ - private static function register_report_csv_export() { - Export::configure(); - } - /** * Load and hydrate the widget type registry. * From cacb7cfc56fac9d92fabeebe89d6399974ae7a22 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Thu, 16 Jul 2026 11:47:42 +1200 Subject: [PATCH 19/34] Premium Analytics: Refresh WPCOMSH blaze lock reference The composer pins revert restored a lock entry pointing at a pre-rebase blaze tree; regenerate against current trunk. --- projects/plugins/wpcomsh/composer.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index dc596b40ab29..fbf1842cfeea 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -469,7 +469,7 @@ "dist": { "type": "path", "url": "../../packages/blaze", - "reference": "6f33631a30fe59d482659c8ce80da39e349a0e83" + "reference": "be03d0b4ed8fc8ed13ac0d9e0a6171553d218eef" }, "require": { "automattic/jetpack-admin-ui": "@dev", From 26712c5a18d3a06f07bbf6e2a9733e9b22f4ea31 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 17 Jul 2026 09:54:41 +1200 Subject: [PATCH 20/34] Premium Analytics: Fix Simple readiness redirect loop on report routes The dynamic report route still read connection/sync state directly from script data instead of the shared site-readiness helpers, so on Simple it always redirected to /connect - which then bounced back to / since /connect already treats Simple as connected. Route every guard through the same isPremiumAnalyticsSiteConnected() / isPremiumAnalyticsInitialSyncFinished() helpers the other four route guards already use. --- .../premium-analytics/routes/reports/route.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/projects/packages/premium-analytics/routes/reports/route.ts b/projects/packages/premium-analytics/routes/reports/route.ts index 8aab5c2c3b6c..878380fea8f5 100644 --- a/projects/packages/premium-analytics/routes/reports/route.ts +++ b/projects/packages/premium-analytics/routes/reports/route.ts @@ -1,12 +1,15 @@ /** * External dependencies */ -import { getScriptData } from '@automattic/jetpack-script-data'; import { ensureCoreSettingsReady, normalizeReportParams } from '@jetpack-premium-analytics/data'; import { redirect } from '@wordpress/route'; /** * Internal dependencies */ +import { + isPremiumAnalyticsInitialSyncFinished, + isPremiumAnalyticsSiteConnected, +} from '../site-readiness'; import { getReportDefinition } from './registry'; type ReportRouteParams = { report?: string }; @@ -39,14 +42,11 @@ export const route = { params, search, }: { params?: ReportRouteParams; search?: ReportRouteSearch } = {} ) => { - const connectionStatus = getScriptData()?.connection?.connectionStatus; - - if ( ! connectionStatus?.isRegistered ) { + if ( ! isPremiumAnalyticsSiteConnected() ) { throw redirect( { to: '/connect' } ); } - const syncFinished = getScriptData()?.premium_analytics?.initial_full_sync_finished ?? 0; - if ( ! syncFinished ) { + if ( ! isPremiumAnalyticsInitialSyncFinished() ) { throw redirect( { to: '/syncing' } ); } From 8a2fd0ec73626a2758ae2d039923cd5576249c70 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 17 Jul 2026 09:54:56 +1200 Subject: [PATCH 21/34] Premium Analytics: Fix calendar-diff timezone bug instead of pinning tests to UTC getDaysBetweenInclusive() and getPeriodsBetweenInclusive() built plain UTC-tagged Date objects but fed them to date-fns's local-calendar diff functions, which read local getters. On a negative-offset machine, a UTC midnight instant landing on a week/month/year boundary reads as the previous local day on only one side of the range, skewing bucket counts (e.g. a 4-week range computing as 5 weeks). Anchor both dates with the existing localTZDate() helper in UTC so the diff is deterministic regardless of the host timezone. Also mock a fixed site timezone in stats-queries.test.ts, matching the convention already used elsewhere in the package, so the WordAds 'yesterday' clamp (which is legitimately site-timezone-dependent) no longer inherits the test machine's real system timezone. Both fixes verified across UTC, negative, and extreme positive offsets; the jest TZ=UTC pin is no longer needed and is removed. --- .../queries/__tests__/stats-queries.test.ts | 22 +++++++++++++++++++ .../packages/data/src/utils/interval.ts | 9 ++++++-- .../packages/data/src/utils/stats-params.ts | 12 ++++++++-- .../premium-analytics/tests/jest.config.cjs | 4 ---- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts b/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts index c50eaaae2f9b..287c735b7b03 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/__tests__/stats-queries.test.ts @@ -1,3 +1,20 @@ +/** + * Mock WordPress dependencies so date.ts can load. The select mock returns + * site settings with timezone: 'UTC' so getSiteTimezone() returns UTC, + * making localTZDate's default (site-timezone) calls deterministic + * regardless of the machine running the test (e.g. the WordAds "yesterday" + * clamp in stats-wordads-query.ts). + */ +jest.mock( '@wordpress/core-data', () => ( { + store: 'core', +} ) ); + +jest.mock( '@wordpress/data', () => ( { + select: jest.fn( () => ( { + getEntityRecord: jest.fn( () => ( { timezone: 'UTC' } ) ), + } ) ), +} ) ); + /** * External dependencies */ @@ -623,6 +640,7 @@ describe( 'Stats query factories', () => { 'GET', { date: '2026-06-16' }, {}, + false, ] ); } ); @@ -747,6 +765,7 @@ describe( 'Stats query factories', () => { 'GET', { site: 41 }, {}, + true, ] ); } ); @@ -759,6 +778,7 @@ describe( 'Stats query factories', () => { 'GET', { type: 'transferred' }, {}, + true, ] ); } ); @@ -1132,6 +1152,7 @@ describe( 'Stats query factories', () => { 'GET', { 'include-pages': true }, {}, + false, ] ); } ); @@ -1145,6 +1166,7 @@ describe( 'Stats query factories', () => { 'GET', {}, {}, + false, ] ); } ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts index dd9fbb7c02ec..00df6a33c092 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts @@ -9,8 +9,13 @@ import { localTZDate } from './date'; import type { IntervalType } from './search'; export function getDaysBetweenInclusive( from: string, to: string ): number { - const fromDate = new Date( `${ from }T00:00:00Z` ); - const toDate = new Date( `${ to }T00:00:00Z` ); + // Anchor both dates in UTC before diffing: `differenceInCalendarDays` reads + // its arguments' local calendar getters, and a plain UTC-tagged `Date`'s + // getters reflect the machine's local timezone, not UTC. Left unanchored, + // a negative-offset machine can read a UTC midnight instant as the + // previous local calendar day, shifting the day count. + const fromDate = localTZDate( `${ from }T00:00:00Z`, '+00:00' ); + const toDate = localTZDate( `${ to }T00:00:00Z`, '+00:00' ); const days = differenceInCalendarDays( toDate, fromDate ); if ( Number.isNaN( days ) || days < 0 ) { diff --git a/projects/packages/premium-analytics/packages/data/src/utils/stats-params.ts b/projects/packages/premium-analytics/packages/data/src/utils/stats-params.ts index 3969972452d6..db7cd18a27b1 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/stats-params.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/stats-params.ts @@ -9,6 +9,7 @@ import { /** * Internal dependencies */ +import { localTZDate } from './date'; import { getDaysBetweenInclusive } from './interval'; import type { ReportParams } from './search'; import type { StatsProxyParams } from '../api/stats-proxy-fetch'; @@ -88,8 +89,15 @@ export function getPeriodsBetweenInclusive( return getDaysBetweenInclusive( from, to ); } - const fromDate = new Date( `${ datePart( from ) }T00:00:00Z` ); - const toDate = new Date( `${ datePart( to ) }T00:00:00Z` ); + // Anchor both dates in UTC before diffing: the calendar-diff functions read + // their arguments' local getters, and a plain UTC-tagged `Date`'s getters + // reflect the machine's local timezone, not UTC. Left unanchored, a + // negative-offset machine can read a UTC midnight instant that lands + // exactly on a week/month/year boundary as the previous local period, + // shifting only one side of the range and skewing the bucket count (e.g. + // a 4-week range reading as 5 weeks). + const fromDate = localTZDate( `${ datePart( from ) }T00:00:00Z`, '+00:00' ); + const toDate = localTZDate( `${ datePart( to ) }T00:00:00Z`, '+00:00' ); const differenceForPeriod = { week: differenceInCalendarISOWeeks, diff --git a/projects/packages/premium-analytics/tests/jest.config.cjs b/projects/packages/premium-analytics/tests/jest.config.cjs index bdecae129c2e..1720850fc917 100644 --- a/projects/packages/premium-analytics/tests/jest.config.cjs +++ b/projects/packages/premium-analytics/tests/jest.config.cjs @@ -1,10 +1,6 @@ const path = require( 'path' ); const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' ); -// Date logic falls back to the machine timezone (Intl.DateTimeFormat), so -// date-window tests only pass in UTC. Pin it to match CI on any machine. -process.env.TZ = 'UTC'; - module.exports = { ...baseConfig, rootDir: path.join( __dirname, '..' ), From 8dba5f40ac373c5561ae78cea72e02d554c5eefc Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 17 Jul 2026 09:55:09 +1200 Subject: [PATCH 22/34] Premium Analytics: Harden the stats proxy transport per review - Require callers to opt into a global (non-site-scoped) WPCOM request via an explicit global flag, instead of inferring it from an endpoint-name match against 'upgrades'. A future endpoint under the same prefix (e.g. upgrades/foo) would otherwise silently miss the global-request handling. - Fail loudly when a global Simple request has no site to scope to, instead of silently sending an unscoped request that could return data across every site the caller can access. - Replace StatsProxyParams' Record with a recursive serializable-query type, restoring compile-time rejection of non-serializable values (functions, Dates, class instances) while still admitting nested filter arrays. - Move getNoticesPath() out of constants.ts and next to resolveStatsProxyRequest(), since it is a runtime resolver, not a static value, and belongs with the rest of the transport logic. --- .../api/__tests__/stats-proxy-fetch.test.ts | 50 +++++++++++-- .../packages/data/src/api/constants.ts | 14 ---- .../data/src/api/stats-proxy-fetch.ts | 72 ++++++++++++++++--- .../src/queries/stats-app-notices-query.ts | 2 +- .../src/queries/stats-app-purchases-query.ts | 2 + .../data/src/queries/stats-app-query.ts | 5 +- 6 files changed, 114 insertions(+), 31 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts index b1a40c9373a9..d17f76293c69 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/__tests__/stats-proxy-fetch.test.ts @@ -101,21 +101,56 @@ describe( 'getStatsProxyPath', () => { ).toBe( '/wpcom/v2/analytics/reports/sessions/by-date?interval=day' ); } ); - it( 'adds the current Simple site query for global upgrades requests', () => { + it( 'adds the current Simple site query for global requests', () => { setSimpleScriptData( 67890 ); - expect( getStatsProxyPath( { version: '1.2', endpoint: '/upgrades' } ) ).toBe( + expect( getStatsProxyPath( { version: '1.2', endpoint: '/upgrades', global: true } ) ).toBe( '/rest/v1.2/upgrades?site=67890' ); } ); - it( 'does not overwrite an explicit upgrades site query on Simple', () => { + it( 'does not overwrite an explicit site query on Simple global requests', () => { setSimpleScriptData( 67890 ); expect( - getStatsProxyPath( { version: '1.2', endpoint: '/upgrades', params: { site: 41 } } ) + getStatsProxyPath( { + version: '1.2', + endpoint: '/upgrades', + params: { site: 41 }, + global: true, + } ) ).toBe( '/rest/v1.2/upgrades?site=41' ); } ); + + it( 'does not scope a request as global on Simple without opting in', () => { + setSimpleScriptData( 67890 ); + + // No `global: true` — treated as an ordinary site-scoped request even + // though the endpoint name is `upgrades`, since the resolver no longer + // infers `global` from the endpoint string. + expect( getStatsProxyPath( { version: '1.2', endpoint: '/upgrades' } ) ).toBe( + '/rest/v1.2/upgrades' + ); + } ); + + it( 'throws for a Simple global request with no site to scope to', () => { + Object.defineProperty( window, 'JetpackScriptData', { + configurable: true, + value: { site: { host: 'wpcom' } }, + } ); + + expect( () => + getStatsProxyPath( { version: '1.2', endpoint: '/upgrades', global: true } ) + ).toThrow( /has no site to scope to/ ); + } ); + + it( 'does not require a site for a non-Simple global request', () => { + // Local proxy path is unaffected by `global` — the flag only changes + // Simple's public-api dispatch, so a missing blog id shouldn't throw here. + expect( getStatsProxyPath( { version: '1.2', endpoint: '/upgrades', global: true } ) ).toBe( + '/jetpack-premium-analytics/v1/proxy/v1.2/upgrades' + ); + } ); } ); describe( 'fetchStatsProxy', () => { @@ -192,21 +227,22 @@ describe( 'fetchStatsProxy', () => { await fetchReport( 'coupons/by-date', { from: '2026-06-01', to: '2026-06-30', - filters: [ { key: 'product', operator: 'is', value: '42' } ], + filters: [ { key: 'product', compare: 'IN', value: '42' } ], } ); const path = ( mockApiFetch.mock.calls[ 0 ][ 0 ] as { path: string } ).path; expect( decodeURIComponent( path ) ).toBe( - '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/coupons/by-date?from=2026-06-01&to=2026-06-30&filters[0][key]=product&filters[0][operator]=is&filters[0][value]=42' + '/jetpack-premium-analytics/v1/proxy/v2/analytics/reports/coupons/by-date?from=2026-06-01&to=2026-06-30&filters[0][key]=product&filters[0][compare]=IN&filters[0][value]=42' ); } ); - it( 'marks WPCOM Simple upgrades requests as global', async () => { + it( 'marks WPCOM Simple upgrades requests as global when opted in', async () => { setSimpleScriptData( 67890 ); await fetchStatsProxy( { version: '1.2', endpoint: 'upgrades', + global: true, } ); expect( mockApiFetch ).toHaveBeenCalledWith( { diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts index 872c8ddfd1a7..a7a0b99d28a6 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/constants.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts @@ -1,18 +1,4 @@ -/** - * External dependencies - */ -import { isSimpleSite } from '@automattic/jetpack-script-data'; - /** * Constants for API endpoints */ export const statsProxyPath = '/jetpack-premium-analytics/v1/proxy'; -export const noticesPath = '/jetpack-premium-analytics/v1/notices'; - -// WPCOM Simple has no local notices endpoint: requests use the WPCOM Stats -// notices endpoint directly, dispatched by WPCOM's wp-admin apiFetch bridge. -export const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; - -export function getNoticesPath() { - return isSimpleSite() ? wpcomSimpleNoticesPath : noticesPath; -} diff --git a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts index 7c433fb903c3..0500db61fab1 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/stats-proxy-fetch.ts @@ -13,7 +13,18 @@ export type StatsProxyVersion = '1.1' | '1.2' | '2'; export type StatsProxyMethod = 'GET' | 'POST'; -export type StatsProxyParams = Record< string, unknown >; +// A JSON-serializable query value: scalars, arrays of scalars (e.g. a +// multi-select param), or an array of flat scalar-valued records (e.g. +// `filters`, a list of `FilterCondition`-shaped objects). Rejects functions, +// Dates, and class instances that `addQueryArgs` can't serialize, while still +// admitting nested filter conditions. +type StatsProxyParamScalar = string | number | boolean | undefined | null; +type StatsProxyParamValue = + | StatsProxyParamScalar + | StatsProxyParamScalar[] + | Record< string, StatsProxyParamScalar | StatsProxyParamScalar[] >[]; + +export type StatsProxyParams = Record< string, StatsProxyParamValue >; export type StatsProxyFetchParams< TBody = unknown > = { version: StatsProxyVersion; @@ -21,6 +32,14 @@ export type StatsProxyFetchParams< TBody = unknown > = { params?: StatsProxyParams; method?: StatsProxyMethod; body?: TBody; + /** + * Marks a request to a global (non-site-scoped) WPCOM public-api endpoint, + * e.g. `upgrades`. Only meaningful on Simple: callers must opt in + * explicitly rather than relying on an endpoint-name match, since a + * future endpoint under the same prefix (e.g. `upgrades/foo`) would + * otherwise silently miss the global-request handling. + */ + global?: boolean; }; type ResolvedStatsProxyRequest = { @@ -50,21 +69,26 @@ function cleanQueryParams( params?: StatsProxyParams ) { * Connected Jetpack sites reach WPCOM through the local Premium Analytics * proxy. WPCOM Simple has no local proxy: requests use public-api namespaces * directly and are dispatched by WPCOM's wp-admin apiFetch bridge, which - * site-scopes every path unless the request is marked `global`. `upgrades` - * is a global public-api endpoint, so it opts out of the path scoping and - * carries the site as a query arg instead. + * site-scopes every path unless the request is marked `global`. A caller + * passing `global: true` (e.g. the `upgrades` endpoint) opts out of that path + * scoping and carries the site as a query arg instead. * * @param request - The stats request to resolve. * @param request.version - WPCOM API version (`1.1`, `1.2`, or `2`). * @param request.endpoint - Endpoint path below the version base. * @param request.params - Query params of the request. + * @param request.global - Whether this targets a global (non-site-scoped) endpoint. * @return The apiFetch path, plus `global: true` for global WPCOM endpoints. */ function resolveStatsProxyRequest( { version, endpoint, params, -}: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > ): ResolvedStatsProxyRequest { + global, +}: Pick< + StatsProxyFetchParams, + 'version' | 'endpoint' | 'params' | 'global' +> ): ResolvedStatsProxyRequest { const normalizedEndpoint = normalizeEndpoint( endpoint ); if ( ! isSimpleSite() ) { @@ -76,12 +100,23 @@ function resolveStatsProxyRequest( { const base = version === '2' ? '/wpcom/v2' : `/rest/v${ version }`; const path = `${ base }/${ normalizedEndpoint }`; - if ( normalizedEndpoint !== 'upgrades' ) { + if ( ! global ) { const queryParams = cleanQueryParams( params ); return { path: queryParams ? addQueryArgs( path, queryParams ) : path }; } const blogId = getScriptData()?.site?.wpcom?.blog_id; + if ( ! blogId && ! params?.site ) { + // A global request with no site to scope to would return data across + // every site the user can access, not this site's — silently wrong + // data rather than a visible error. Fail loudly instead: this surfaces + // as a query error the widget's error state can show and retry. + throw new Error( + `Global WPCOM Simple request to "${ normalizedEndpoint }" has no site to scope to: ` + + 'JetpackScriptData.site.wpcom.blog_id is missing and no site param was provided.' + ); + } + const queryParams = cleanQueryParams( blogId && ! params?.site ? { ...params, site: blogId } : params ); @@ -92,11 +127,26 @@ function resolveStatsProxyRequest( { } export function getStatsProxyPath( - request: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' > + request: Pick< StatsProxyFetchParams, 'version' | 'endpoint' | 'params' | 'global' > ) { return resolveStatsProxyRequest( request ).path; } +const localNoticesPath = '/jetpack-premium-analytics/v1/notices'; + +// WPCOM Simple has no local notices endpoint: requests use the WPCOM Stats +// notices endpoint directly, dispatched by WPCOM's wp-admin apiFetch bridge. +const wpcomSimpleNoticesPath = '/wpcom/v2/jetpack-stats-dashboard/notices'; + +/** + * Resolve the notices endpoint path for the current environment. + * + * @return The notices path. + */ +export function getNoticesPath() { + return isSimpleSite() ? wpcomSimpleNoticesPath : localNoticesPath; +} + /** * Fetch a Woo analytics report through the stats transport. * @@ -121,8 +171,14 @@ export async function fetchStatsProxy< TResponse = unknown, TBody = unknown >( { params, method = 'GET', body, + global, }: StatsProxyFetchParams< TBody > ): Promise< TResponse > { - const { path, global: isGlobal } = resolveStatsProxyRequest( { version, endpoint, params } ); + const { path, global: isGlobal } = resolveStatsProxyRequest( { + version, + endpoint, + params, + global, + } ); return apiFetch< TResponse >( { path, diff --git a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts index 457e712b46d7..0e7a5f7f3d9c 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-notices-query.ts @@ -1,6 +1,6 @@ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import { getNoticesPath } from '../api/constants'; +import { getNoticesPath } from '../api/stats-proxy-fetch'; import { statsAppQueryKeyPart } from './stats-app-query'; import type { UseQueryOptions } from '@tanstack/react-query'; diff --git a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-purchases-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-purchases-query.ts index 35da12dc2ba0..78b4a5f47d1a 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-purchases-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-purchases-query.ts @@ -55,4 +55,6 @@ export const statsAppPurchasesQuery = ( params: StatsAppPurchasesParams = {} ) = version: '1.2', endpoint: 'upgrades', params, + // `upgrades` is a global (non-site-scoped) public-api endpoint on Simple. + global: true, } ); diff --git a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-query.ts index dbd65a6e6442..f0d1b932cc8e 100644 --- a/projects/packages/premium-analytics/packages/data/src/queries/stats-app-query.ts +++ b/projects/packages/premium-analytics/packages/data/src/queries/stats-app-query.ts @@ -18,6 +18,7 @@ type StatsAppQueryConfig = { params?: StatsProxyParams; method?: StatsProxyMethod; body?: unknown; + global?: boolean; }; // App/admin resources use the Stats proxy transport but do not use report param @@ -29,6 +30,7 @@ export function statsAppProxyQuery< TData = unknown >( { params, method = 'GET', body, + global, }: StatsAppQueryConfig ): UseQueryOptions< TData > { return { queryKey: [ @@ -39,8 +41,9 @@ export function statsAppProxyQuery< TData = unknown >( { method, statsAppQueryKeyPart( params ), statsAppQueryKeyPart( body ), + global ?? false, ], - queryFn: () => fetchStatsProxy< TData >( { version, endpoint, params, method, body } ), + queryFn: () => fetchStatsProxy< TData >( { version, endpoint, params, method, body, global } ), placeholderData: previousData => previousData, }; } From f4d6211586d13e8a3ba1c120e6612b957984de09 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Fri, 17 Jul 2026 09:55:18 +1200 Subject: [PATCH 23/34] Docs: Explain composer versions pins for branchless CI builds Leaves a breadcrumb for the next contributor who adds a path dependency and hits the same WPCOMSH-style composer solve failure on a detached-HEAD CI checkout with no COMPOSER_ROOT_VERSION override. --- docs/monorepo.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/monorepo.md b/docs/monorepo.md index 3b9842ac0272..65a115e49f16 100644 --- a/docs/monorepo.md +++ b/docs/monorepo.md @@ -165,6 +165,7 @@ The build commands should assume that `pnpm install` and `composer install` have * If you're building JavaScript bundles with Webpack and [@automattic/jetpack-webpack-config](../projects/js-packages/webpack-config/README.md) (more information on setup [in the README.md](../projects/js-packages/webpack-config/README.md)), note that your build-production command should set `NODE_ENV=production` and `BABEL_ENV=production`. * If you run into problems with Composer not recognizing the local git branch as being the right version, try setting `COMPOSER_ROOT_VERSION=dev-trunk` in the environment. +* Some CI jobs (e.g. the WPCOMSH WP Cloud build) check out a project on a branchless, detached-HEAD commit and run a plain `composer update` with no `COMPOSER_ROOT_VERSION` override. There, Composer's version guesser can't label sibling path packages `dev-trunk`, so a third-party dependency with a caret constraint against one of those packages (e.g. `automattic/woocommerce-analytics` against `automattic/jetpack-assets`) fails to resolve — the path package's guessed version doesn't satisfy the constraint, and Composer refuses to fall back to Packagist because the path repo is canonical. If you hit this, pin the affected packages to `dev-trunk` explicitly via `options.versions` on the project's monorepo path repository in `composer.json` (see `projects/packages/premium-analytics/composer.json`, `projects/packages/jetpack-mu-wpcom/composer.json`, and `projects/plugins/wpcomsh/composer.json` for the pattern, added for `automattic/woocommerce-analytics`'s dependencies in Automattic/jetpack#50266). This mirrors what `tools/cli/commands/build.js` injects automatically for `--for-mirrors` builds — those pins just aren't needed for every other CI path, so add only what a specific failing job requires. * When building for the mirror repos, note that `COMPOSER_MIRROR_PATH_REPOS=1` will be set in the environment and the list of repositories in `composer.json` may be altered. This is not normally done in development environments, even with `jetpack build --production`. * For a production build of a plugin, `composer install` is passed `-o --no-dev --classmap-authoritative --prefer-dist`. For development builds and for production builds of non-plugin projects, no options are passed. From 0b037551655f0fa9b589109c65001c325b0a6253 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 09:32:39 +1200 Subject: [PATCH 24/34] Premium Analytics: Expose a single entry point for the WPCOM dashboard routes WPCOM's public-api loader previously required several internal PA files directly, in a specific order, and referenced the three route registrar functions by name - a future internal refactor could break that coupling silently. Dashboard_Support_Routes::register_for_wpcom() is now the one public, stable call a host needs: it owns the file list, load order, and function names internally. Analytics::init() calls the same method for connected sites, so there is one implementation of the registration, not two. --- .../premium-analytics/src/class-analytics.php | 8 +-- .../src/class-dashboard-support-routes.php | 59 +++++++++++++++ .../php/Dashboard_Support_Routes_Test.php | 72 +++++++++++++++++++ 3 files changed, 134 insertions(+), 5 deletions(-) create mode 100644 projects/packages/premium-analytics/src/class-dashboard-support-routes.php create mode 100644 projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 3ffdee7ee2a3..e827f10abb35 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -193,15 +193,13 @@ private static function load_dashboard_components() { * Serve the dashboard support routes (widget modules, default layout, * sections) from the site. * - * Simple skips this: WPCOM registers these endpoints separately on - * public-api, and Simple dashboard requests are dispatched there. + * Simple skips this: WPCOM calls Dashboard_Support_Routes::register_for_wpcom() + * directly from its own public-api process instead. * * @return void */ private static function register_dashboard_support_routes() { - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); + Dashboard_Support_Routes::register_for_wpcom(); } /** diff --git a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php new file mode 100644 index 000000000000..fe758b1258ed --- /dev/null +++ b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php @@ -0,0 +1,59 @@ +get_routes(); + + $this->assertArrayHasKey( '/wpcom/v2/widget-modules', $routes ); + $this->assertArrayHasKey( + '/wpcom/v2/dashboards/(?P[a-z][a-z0-9-]*(?:_[a-z0-9-]+)*)/default-layout', + $routes + ); + $this->assertArrayHasKey( '/wpcom/v2/dashboards/(?P[a-z][a-z0-9-]*(?:_[a-z0-9-]+)*)/sections', $routes ); + } + + /** + * Calling it a second time (e.g. from a second host hook) does not + * register a route twice or error. + */ + public function test_register_for_wpcom_is_safe_to_call_twice() { + Dashboard_Support_Routes::register_for_wpcom(); + Dashboard_Support_Routes::register_for_wpcom(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + do_action( 'init' ); + do_action( 'rest_api_init' ); + + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wpcom/v2/widget-modules', $routes ); + $this->assertCount( 1, $routes['/wpcom/v2/widget-modules'] ); + } +} From beca3cd7a83aa0cacc9573644f4428b3151a0b48 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 09:42:01 +1200 Subject: [PATCH 25/34] Premium Analytics: Simplify the boot sequence and route-registration API - Extract boot_shared_services() so the two init entry points show only their real platform difference (sync bootstrap, local API, dashboard support routes), instead of repeating the four identical steps both share. - Rename Dashboard_Support_Routes::register_for_wpcom() to register(): the method also serves connected Jetpack sites, so the WPCOM-specific name was misleading at its own call site. Matches this package's existing ::register() convention (Sync\Configuration, REST\Notices_Controller, REST\Api_Proxy_Controller). --- .../premium-analytics/src/class-analytics.php | 42 ++++++++++--------- .../src/class-dashboard-support-routes.php | 6 ++- .../php/Dashboard_Support_Routes_Test.php | 12 +++--- 3 files changed, 33 insertions(+), 27 deletions(-) diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index e827f10abb35..1fbb9238b511 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -62,15 +62,7 @@ public static function init( $options = array() ) { // Piggybacks on the Jetpack Stats module; checks Jetpack connection state. Jetpack_Stats_Tracker::configure(); - // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). - WooCommerce_Analytics_Tracker::configure(); - - // 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::load_widget_registry(); - self::load_dashboard_components(); + self::boot_shared_services(); self::register_dashboard_support_routes(); self::load_build(); self::register_admin_page(); @@ -97,15 +89,7 @@ public static function init_wpcom_simple( $options = array() ) { self::$initialized = true; self::apply_options( $options ); - // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). - WooCommerce_Analytics_Tracker::configure(); - - // 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::load_widget_registry(); - self::load_dashboard_components(); + self::boot_shared_services(); self::load_build(); self::register_admin_page(); } @@ -122,6 +106,24 @@ private static function apply_options( $options ) { } } + /** + * Boot the services and registries every platform needs, regardless of + * whether the site serves the dashboard support routes itself. + * + * @return void + */ + private static function boot_shared_services() { + // Emit WooCommerce store events into the Woo pipeline (ClickHouse + proxy). + WooCommerce_Analytics_Tracker::configure(); + + // 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::load_widget_registry(); + self::load_dashboard_components(); + } + /** * Register the sync services that feed the local data pipeline. * @@ -193,13 +195,13 @@ private static function load_dashboard_components() { * Serve the dashboard support routes (widget modules, default layout, * sections) from the site. * - * Simple skips this: WPCOM calls Dashboard_Support_Routes::register_for_wpcom() + * Simple skips this: WPCOM calls Dashboard_Support_Routes::register() * directly from its own public-api process instead. * * @return void */ private static function register_dashboard_support_routes() { - Dashboard_Support_Routes::register_for_wpcom(); + Dashboard_Support_Routes::register(); } /** diff --git a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php index fe758b1258ed..f293efd78560 100644 --- a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php +++ b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php @@ -20,6 +20,10 @@ * those internals doesn't require a coordinated change on the WPCOM side. * `Analytics::init()` calls the same method for connected Jetpack sites, so * there is one implementation of "how to register these routes", not two. + * + * Despite the WPCOM motivation, the method itself isn't WPCOM-specific — it + * registers routes that both platforms serve — so it follows this package's + * plain `::register()` convention (see Sync\Configuration, REST\Notices_Controller). */ class Dashboard_Support_Routes { @@ -34,7 +38,7 @@ class Dashboard_Support_Routes { * * @return void */ - public static function register_for_wpcom() { + public static function register() { // Defines jpa_get_registered_widget_modules(), the manifest // register_widget_types() below reads. Guarded: absent in // environments without a JS build (e.g. some PHPUnit runs); diff --git a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php index fc276bc29c03..092e619f2b2f 100644 --- a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php +++ b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php @@ -28,13 +28,13 @@ protected function tearDown(): void { /** * A host that has loaded nothing else from this package can call - * register_for_wpcom() and get all three dashboard support routes. + * register() and get all three dashboard support routes. * * This is the exact contract WPCOM's public-api process relies on: it * never calls Analytics::init(), only this one method. */ - public function test_register_for_wpcom_registers_all_three_routes() { - Dashboard_Support_Routes::register_for_wpcom(); + public function test_register_registers_all_three_routes() { + Dashboard_Support_Routes::register(); global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); @@ -55,9 +55,9 @@ public function test_register_for_wpcom_registers_all_three_routes() { * Calling it a second time (e.g. from a second host hook) does not * register a route twice or error. */ - public function test_register_for_wpcom_is_safe_to_call_twice() { - Dashboard_Support_Routes::register_for_wpcom(); - Dashboard_Support_Routes::register_for_wpcom(); + public function test_register_is_safe_to_call_twice() { + Dashboard_Support_Routes::register(); + Dashboard_Support_Routes::register(); global $wp_rest_server; $wp_rest_server = new WP_REST_Server(); From 123af62c7da9780e11c7b49e3791b8786d63332e Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 09:46:13 +1200 Subject: [PATCH 26/34] Premium Analytics: Tighten comments, document the WPCOM call contract Trim the boot-step and Dashboard_Support_Routes comments to one line each; move the ::register() vs ::register_for_wpcom() naming rationale out of the class docblock. Add an AGENTS.md section naming the exact method WPCOM's public-api process calls and the exact file to update on the WPCOM side if it ever changes. --- projects/packages/premium-analytics/AGENTS.md | 22 ++++++++++ .../premium-analytics/src/class-analytics.php | 40 +++++++------------ .../src/class-dashboard-support-routes.php | 30 ++++---------- .../php/Dashboard_Support_Routes_Test.php | 10 ++--- 4 files changed, 48 insertions(+), 54 deletions(-) diff --git a/projects/packages/premium-analytics/AGENTS.md b/projects/packages/premium-analytics/AGENTS.md index 69f8c97386eb..111342965cf9 100644 --- a/projects/packages/premium-analytics/AGENTS.md +++ b/projects/packages/premium-analytics/AGENTS.md @@ -101,6 +101,28 @@ 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 — the dashboard is served +by WPCOM and reaches `public-api.wordpress.com` directly via WPCOM's own apiFetch bridge. +`jetpack-mu-wpcom` boots the package with `Analytics::init_wpcom_simple()` behind the +`jetpack-premium-analytics` blog sticker (see `Jetpack_Mu_Wpcom::load_wpcom_simple_premium_analytics()`). + +**The one method WPCOM calls directly: `Dashboard_Support_Routes::register()`** +(`src/class-dashboard-support-routes.php`). It registers the dashboard's REST support routes +(widget modules, default layout, sections) standalone, without booting the rest of the package — +this is what makes those routes exist in WPCOM's own public-api process, separate from the site. +`Analytics::init()` calls the same method for connected sites, so there is one implementation, not +two. + +**If you rename this method, move it, or change its parameters or namespace, you must update the +WPCOM-side caller in the same PR round** (or coordinate a WPCOM-side follow-up before merging): +`wp-content/rest-api-plugins/jetpack-endpoints/premium-analytics-dashboard.php` in the `wpcom` +repo. That file `require_once`s this class by its exact path and calls `::register()` by name +behind an `is_callable()` guard — it has no other knowledge of this package's internals, which is +the point, but it does hard-depend on this one call staying valid. See Automattic/jetpack#50266 +and the `wpcom` PR it links for the change that established this contract. + ## Pitfalls - Never put the blog ID in a proxy path — it's injected server-side. diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index 1fbb9238b511..ebe699496b7f 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -71,11 +71,9 @@ public static function init( $options = array() ) { /** * Initialize the Analytics app on WordPress.com Simple. * - * Simple serves the dashboard from WPCOM and uses WPCOM's wp-admin apiFetch - * bridge to reach public-api.wordpress.com directly, so it registers none of - * the site-served REST surfaces a connected Jetpack site provides: no proxy, - * no notices, no sync bootstrap, and no dashboard support routes — WPCOM - * registers those endpoints separately on public-api. + * Simple reaches public-api.wordpress.com directly via WPCOM's apiFetch + * bridge, so it registers no local REST surface: no proxy, notices, sync + * bootstrap, or dashboard support routes. WPCOM registers those separately. * * @param array $options Optional configuration options. * Supported keys: @@ -158,15 +156,13 @@ private static function register_local_api() { * @return void */ private static function load_widget_registry() { - // Load the widget type registry: hydration routine, registry-time and - // runtime filters, and the registry accessors. + // Registry, accessors, and registry-time/runtime filters. require_once __DIR__ . '/widget-types.php'; - // Apply Premium Analytics' availability policy: hooks the registry-time - // filter to keep developer-only types out of production. + // Hides developer-only widget types from production. require_once __DIR__ . '/widget-availability.php'; - // Hydrate the registry with the availability filter in place. + // Hydrate the registry with the availability filter already in place. bootstrap_widget_types(); } @@ -176,27 +172,23 @@ private static function load_widget_registry() { * @return void */ private static function load_dashboard_components() { - // Wire dashboard widget modules into the page import map for dynamic - // import() on the client. + // Widget modules for the client's dynamic import() map. require_once __DIR__ . '/widget-modules.php'; - // Register the dashboard's default layout first-load preference injection. + // Default layout's first-load preference injection. require_once __DIR__ . '/dashboard-layout.php'; - // Register dashboard sections and their default layout seeding. + // Dashboard sections and their default layout seeding. require_once __DIR__ . '/dashboard-sections.php'; - // Expose opt-in CSV export settings to the dashboard. + // Opt-in CSV export settings. require_once __DIR__ . '/csv-exports.php'; configure_csv_exports(); } /** - * Serve the dashboard support routes (widget modules, default layout, - * sections) from the site. - * - * Simple skips this: WPCOM calls Dashboard_Support_Routes::register() - * directly from its own public-api process instead. + * Serve the dashboard support routes from the site. Simple skips this — + * WPCOM calls Dashboard_Support_Routes::register() itself instead. * * @return void */ @@ -207,11 +199,9 @@ private static function register_dashboard_support_routes() { /** * Load the wp-build output (interceptor, modules, routes, page render). * - * Must run before the is_admin() gate: build/widgets.php defines the - * manifest the widget registry reads, and the registry serves REST requests - * (e.g. /wpcom/v2/widget-modules) where is_admin() is false. The render - * pieces here self-gate on admin_init, so loading them globally is inert - * off the dashboard. + * Must run before the is_admin() gate: the registry serves REST requests + * too (is_admin() false there). Render pieces self-gate on admin_init, so + * this is inert off the dashboard. * * @return void */ diff --git a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php index f293efd78560..030c8083cf6e 100644 --- a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php +++ b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php @@ -10,39 +10,25 @@ /** * Registers the dashboard's REST support routes (widget modules, default - * layout, sections) on a host that never calls Analytics::init(). + * layout, sections) for a host that never calls Analytics::init(). * - * WordPress.com Simple serves the Premium Analytics dashboard from WPCOM - * rather than from the site itself (see Analytics::init_wpcom_simple()), so - * these routes need to exist in WPCOM's own public-api process instead. This - * class is the single entry point for that: it owns the file list, load - * order, and function names the routes depend on, so a future refactor of - * those internals doesn't require a coordinated change on the WPCOM side. - * `Analytics::init()` calls the same method for connected Jetpack sites, so - * there is one implementation of "how to register these routes", not two. - * - * Despite the WPCOM motivation, the method itself isn't WPCOM-specific — it - * registers routes that both platforms serve — so it follows this package's - * plain `::register()` convention (see Sync\Configuration, REST\Notices_Controller). + * WordPress.com Simple serves the dashboard from WPCOM, not the site (see + * Analytics::init_wpcom_simple()), so WPCOM's public-api process calls this + * directly — see AGENTS.md for the call site and what to update on the WPCOM + * side if this changes. Analytics::init() calls the same method for + * connected sites, so there's one implementation, not two. */ class Dashboard_Support_Routes { /** * Register the dashboard support routes. * - * Safe to call from a process that has otherwise loaded none of this - * package, and safe to call more than once: every step here is already - * idempotent on its own (require_once, the widget registry's own - * is_registered() check, and WordPress's de-duplication of an identical - * add_action() callback), so no extra guard is needed here. + * Idempotent: safe to call standalone, and safe to call more than once. * * @return void */ public static function register() { - // Defines jpa_get_registered_widget_modules(), the manifest - // register_widget_types() below reads. Guarded: absent in - // environments without a JS build (e.g. some PHPUnit runs); - // register_widget_types() itself guards its use of the manifest. + // Manifest register_widget_types() reads; absent without a JS build. $widgets_manifest = __DIR__ . '/../build/widgets.php'; if ( file_exists( $widgets_manifest ) ) { require_once $widgets_manifest; diff --git a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php index 092e619f2b2f..a30ab5e7477c 100644 --- a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php +++ b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php @@ -27,11 +27,8 @@ protected function tearDown(): void { } /** - * A host that has loaded nothing else from this package can call - * register() and get all three dashboard support routes. - * - * This is the exact contract WPCOM's public-api process relies on: it - * never calls Analytics::init(), only this one method. + * Standalone register() registers all three routes — the contract + * WPCOM's public-api process relies on. */ public function test_register_registers_all_three_routes() { Dashboard_Support_Routes::register(); @@ -52,8 +49,7 @@ public function test_register_registers_all_three_routes() { } /** - * Calling it a second time (e.g. from a second host hook) does not - * register a route twice or error. + * Calling it twice doesn't register a route twice or error. */ public function test_register_is_safe_to_call_twice() { Dashboard_Support_Routes::register(); From af2a3f5563b25bba3f65f315277e5b2b7d7f73a9 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 09:47:01 +1200 Subject: [PATCH 27/34] Premium Analytics: Simplify the WordPress.com Simple AGENTS.md section --- projects/packages/premium-analytics/AGENTS.md | 29 +++++++------------ 1 file changed, 11 insertions(+), 18 deletions(-) diff --git a/projects/packages/premium-analytics/AGENTS.md b/projects/packages/premium-analytics/AGENTS.md index 111342965cf9..779d56470477 100644 --- a/projects/packages/premium-analytics/AGENTS.md +++ b/projects/packages/premium-analytics/AGENTS.md @@ -103,25 +103,18 @@ prefixes; Woo `analytics/reports/*` → `proxy/v2/analytics/reports/*`. The dash ## WordPress.com Simple -Simple has no local proxy, notices, sync, or dashboard support routes — the dashboard is served -by WPCOM and reaches `public-api.wordpress.com` directly via WPCOM's own apiFetch bridge. -`jetpack-mu-wpcom` boots the package with `Analytics::init_wpcom_simple()` behind the -`jetpack-premium-analytics` blog sticker (see `Jetpack_Mu_Wpcom::load_wpcom_simple_premium_analytics()`). - -**The one method WPCOM calls directly: `Dashboard_Support_Routes::register()`** -(`src/class-dashboard-support-routes.php`). It registers the dashboard's REST support routes -(widget modules, default layout, sections) standalone, without booting the rest of the package — -this is what makes those routes exist in WPCOM's own public-api process, separate from the site. -`Analytics::init()` calls the same method for connected sites, so there is one implementation, not -two. - -**If you rename this method, move it, or change its parameters or namespace, you must update the -WPCOM-side caller in the same PR round** (or coordinate a WPCOM-side follow-up before merging): +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. + +**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. That file `require_once`s this class by its exact path and calls `::register()` by name -behind an `is_callable()` guard — it has no other knowledge of this package's internals, which is -the point, but it does hard-depend on this one call staying valid. See Automattic/jetpack#50266 -and the `wpcom` PR it links for the change that established this contract. +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 From 574f44641e0d7d8cd284bf26d208d6e066dfa834 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:08:20 +1200 Subject: [PATCH 28/34] Premium Analytics: Refresh composer locks after rebasing onto trunk The rebase merged trunk's new jetpack-status dependency and updated package reference hashes into premium-analytics' composer.json, which the three downstream plugin locks hadn't picked up yet. --- projects/plugins/mu-wpcom-plugin/composer.lock | 3 ++- projects/plugins/premium-analytics/composer.lock | 2 +- projects/plugins/wpcomsh/composer.lock | 7 ++++--- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/projects/plugins/mu-wpcom-plugin/composer.lock b/projects/plugins/mu-wpcom-plugin/composer.lock index 41e6b84f1bfe..f8db21fd8f87 100644 --- a/projects/plugins/mu-wpcom-plugin/composer.lock +++ b/projects/plugins/mu-wpcom-plugin/composer.lock @@ -1727,13 +1727,14 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" + "reference": "8a37e653800a0f6eab73996b880000796cd0ff4c" }, "require": { "automattic/jetpack-assets": "@dev", "automattic/jetpack-config": "@dev", "automattic/jetpack-connection": "@dev", "automattic/jetpack-stats": "@dev", + "automattic/jetpack-status": "@dev", "automattic/jetpack-sync": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "automattic/woocommerce-analytics": "^0.16", diff --git a/projects/plugins/premium-analytics/composer.lock b/projects/plugins/premium-analytics/composer.lock index ad94511f0030..361585882483 100644 --- a/projects/plugins/premium-analytics/composer.lock +++ b/projects/plugins/premium-analytics/composer.lock @@ -841,7 +841,7 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "1b3067feb4367b82e67cde8076b9199bcbe44fb3" + "reference": "8a37e653800a0f6eab73996b880000796cd0ff4c" }, "require": { "automattic/jetpack-assets": "@dev", diff --git a/projects/plugins/wpcomsh/composer.lock b/projects/plugins/wpcomsh/composer.lock index fbf1842cfeea..d2cd84ca5c9b 100644 --- a/projects/plugins/wpcomsh/composer.lock +++ b/projects/plugins/wpcomsh/composer.lock @@ -1027,7 +1027,7 @@ "dist": { "type": "path", "url": "../../packages/explat", - "reference": "104841325e090f4137640ab5b09373de64d6ead9" + "reference": "d0bc548ee29c5ba855d266114ab41d469d33cba5" }, "require": { "automattic/jetpack-connection": "@dev", @@ -1700,7 +1700,7 @@ "dist": { "type": "path", "url": "../../packages/podcast", - "reference": "cdeaa612312a11508d74183eae8f6374d87123ac" + "reference": "3e434b0a0774d49bc15913fbb7df57f831dfbe10" }, "require": { "automattic/jetpack-admin-ui": "@dev", @@ -1908,13 +1908,14 @@ "dist": { "type": "path", "url": "../../packages/premium-analytics", - "reference": "b3511e8ca26dd4052147f368ebf1b0aa9179a787" + "reference": "2e38923863dbc23890ece6403cd0ad2fb153b650" }, "require": { "automattic/jetpack-assets": "@dev", "automattic/jetpack-config": "@dev", "automattic/jetpack-connection": "@dev", "automattic/jetpack-stats": "@dev", + "automattic/jetpack-status": "@dev", "automattic/jetpack-sync": "@dev", "automattic/jetpack-wp-build-polyfills": "@dev", "automattic/woocommerce-analytics": "^0.16", From 1f7267da49120135980ef66f4474585ef5766f80 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:19:30 +1200 Subject: [PATCH 29/34] Revert docs/monorepo.md composer versions pins explanation --- docs/monorepo.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/monorepo.md b/docs/monorepo.md index 65a115e49f16..3b9842ac0272 100644 --- a/docs/monorepo.md +++ b/docs/monorepo.md @@ -165,7 +165,6 @@ The build commands should assume that `pnpm install` and `composer install` have * If you're building JavaScript bundles with Webpack and [@automattic/jetpack-webpack-config](../projects/js-packages/webpack-config/README.md) (more information on setup [in the README.md](../projects/js-packages/webpack-config/README.md)), note that your build-production command should set `NODE_ENV=production` and `BABEL_ENV=production`. * If you run into problems with Composer not recognizing the local git branch as being the right version, try setting `COMPOSER_ROOT_VERSION=dev-trunk` in the environment. -* Some CI jobs (e.g. the WPCOMSH WP Cloud build) check out a project on a branchless, detached-HEAD commit and run a plain `composer update` with no `COMPOSER_ROOT_VERSION` override. There, Composer's version guesser can't label sibling path packages `dev-trunk`, so a third-party dependency with a caret constraint against one of those packages (e.g. `automattic/woocommerce-analytics` against `automattic/jetpack-assets`) fails to resolve — the path package's guessed version doesn't satisfy the constraint, and Composer refuses to fall back to Packagist because the path repo is canonical. If you hit this, pin the affected packages to `dev-trunk` explicitly via `options.versions` on the project's monorepo path repository in `composer.json` (see `projects/packages/premium-analytics/composer.json`, `projects/packages/jetpack-mu-wpcom/composer.json`, and `projects/plugins/wpcomsh/composer.json` for the pattern, added for `automattic/woocommerce-analytics`'s dependencies in Automattic/jetpack#50266). This mirrors what `tools/cli/commands/build.js` injects automatically for `--for-mirrors` builds — those pins just aren't needed for every other CI path, so add only what a specific failing job requires. * When building for the mirror repos, note that `COMPOSER_MIRROR_PATH_REPOS=1` will be set in the environment and the list of repositories in `composer.json` may be altered. This is not normally done in development environments, even with `jetpack build --production`. * For a production build of a plugin, `composer install` is passed `-o --no-dev --classmap-authoritative --prefer-dist`. For development builds and for production builds of non-plugin projects, no options are passed. From 20a99796ef10b584b02c7f17206bc321349f1d01 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:19:31 +1200 Subject: [PATCH 30/34] Premium Analytics: Suppress Phan duplicate-statement false positive in test test_register_is_safe_to_call_twice() intentionally calls register() twice in a row to verify the second call is a no-op; Phan's PhanPluginDuplicateAdjacentStatement plugin otherwise flags this as a likely copy-paste bug. --- .../tests/php/Dashboard_Support_Routes_Test.php | 1 + 1 file changed, 1 insertion(+) diff --git a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php index a30ab5e7477c..0f45058a2115 100644 --- a/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php +++ b/projects/packages/premium-analytics/tests/php/Dashboard_Support_Routes_Test.php @@ -53,6 +53,7 @@ public function test_register_registers_all_three_routes() { */ public function test_register_is_safe_to_call_twice() { Dashboard_Support_Routes::register(); + // @phan-suppress-next-line PhanPluginDuplicateAdjacentStatement -- Intentional: testing that a second call is a no-op. Dashboard_Support_Routes::register(); global $wp_rest_server; From 719938d5969d02fc062351985b91519c68da88c0 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:23:59 +1200 Subject: [PATCH 31/34] Premium Analytics: Document why dashboard routes use wpcom/v2, not jetpack/v4 Explains the WPCOM REST centralization constraint that forced the rename and gives explicit guidance for future endpoints: anything that needs to reach both connected Jetpack sites and WPCOM Simple must register under wpcom/v2 via DASHBOARD_REST_NAMESPACE; connected-only endpoints can stay under jetpack-premium-analytics/v1. --- projects/packages/premium-analytics/AGENTS.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/projects/packages/premium-analytics/AGENTS.md b/projects/packages/premium-analytics/AGENTS.md index 779d56470477..e3b72fe3c561 100644 --- a/projects/packages/premium-analytics/AGENTS.md +++ b/projects/packages/premium-analytics/AGENTS.md @@ -107,6 +107,24 @@ Simple has no local proxy, notices, sync, or dashboard support routes — WPCOM 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. +### 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//...` 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. + +**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 From e4b465483d10626220fa58f2a9988113fdc421d1 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:27:13 +1200 Subject: [PATCH 32/34] Premium Analytics: Give the new-endpoint namespace guidance its own section --- projects/packages/premium-analytics/AGENTS.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/projects/packages/premium-analytics/AGENTS.md b/projects/packages/premium-analytics/AGENTS.md index e3b72fe3c561..2f2d21fc93b6 100644 --- a/projects/packages/premium-analytics/AGENTS.md +++ b/projects/packages/premium-analytics/AGENTS.md @@ -119,6 +119,8 @@ public-api, with no separate dotcom-side registration and no `wpcom_rest_api_v2_ 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 From 31982020b0807d1b77e1548ae90b91b15530288c Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 10:55:13 +1200 Subject: [PATCH 33/34] Premium Analytics: Document the shared site-readiness helper requirement Explains the redirect-loop bug chihsuan found on the /reports route (one route reading connection state directly while the others used the shared isPremiumAnalyticsSiteConnected() helpers) and gives future route authors a concrete check: grep routes/ for the helper and match its shape rather than re-deriving the check from script data. --- projects/packages/premium-analytics/AGENTS.md | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/projects/packages/premium-analytics/AGENTS.md b/projects/packages/premium-analytics/AGENTS.md index 2f2d21fc93b6..743f46b0031b 100644 --- a/projects/packages/premium-analytics/AGENTS.md +++ b/projects/packages/premium-analytics/AGENTS.md @@ -107,6 +107,27 @@ Simple has no local proxy, notices, sync, or dashboard support routes — WPCOM 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 From f0a418e82479804271a50f745f05e27326d633fc Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Mon, 20 Jul 2026 11:29:45 +1200 Subject: [PATCH 34/34] Premium Analytics: Lazy-load the widget registry and route files Both Analytics::init() (every request on a connected site) and Dashboard_Support_Routes::register() (every public-api request across all of WPCOM, on Simple) used to eagerly parse the widget manifest, hydrate the widget type registry, and require dashboard-layout.php / dashboard-sections.php - regardless of whether the request would ever read any of it. - widget-modules.php now hydrates the registry itself, lazily, inside its own REST callback and boot-deps filter - the only two readers. Analytics::load_widget_registry() is removed; nothing else needed it. - Dashboard_Support_Routes::register() now only hooks rest_api_init; the actual file loads and route registration moved to a new boot_routes() method that runs there instead, so a request that never dispatches a REST route never pays for any of this. --- .../premium-analytics/src/class-analytics.php | 17 --------- .../src/class-dashboard-support-routes.php | 33 ++++++++++------- .../premium-analytics/src/widget-modules.php | 35 +++++++++++++++++++ .../tests/php/Analytics_Test.php | 25 +++++++++---- .../tests/php/Widget_Modules_Test.php | 13 +++++++ 5 files changed, 88 insertions(+), 35 deletions(-) diff --git a/projects/packages/premium-analytics/src/class-analytics.php b/projects/packages/premium-analytics/src/class-analytics.php index ebe699496b7f..95909a825a9c 100644 --- a/projects/packages/premium-analytics/src/class-analytics.php +++ b/projects/packages/premium-analytics/src/class-analytics.php @@ -118,7 +118,6 @@ private static function boot_shared_services() { // register on all requests. Self-gates on WooCommerce + Jetpack connection. Export::configure(); - self::load_widget_registry(); self::load_dashboard_components(); } @@ -150,22 +149,6 @@ private static function register_local_api() { Notices_Controller::register(); } - /** - * Load and hydrate the widget type registry. - * - * @return void - */ - private static function load_widget_registry() { - // Registry, accessors, and registry-time/runtime filters. - require_once __DIR__ . '/widget-types.php'; - - // Hides developer-only widget types from production. - require_once __DIR__ . '/widget-availability.php'; - - // Hydrate the registry with the availability filter already in place. - bootstrap_widget_types(); - } - /** * Load the dashboard components every platform renders with. * diff --git a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php index 030c8083cf6e..e24faf7229d6 100644 --- a/projects/packages/premium-analytics/src/class-dashboard-support-routes.php +++ b/projects/packages/premium-analytics/src/class-dashboard-support-routes.php @@ -28,22 +28,31 @@ class Dashboard_Support_Routes { * @return void */ public static function register() { - // Manifest register_widget_types() reads; absent without a JS build. - $widgets_manifest = __DIR__ . '/../build/widgets.php'; - if ( file_exists( $widgets_manifest ) ) { - require_once $widgets_manifest; - } - - require_once __DIR__ . '/widget-types.php'; - require_once __DIR__ . '/widget-availability.php'; - bootstrap_widget_types(); + add_action( 'rest_api_init', array( __CLASS__, 'boot_routes' ) ); + } + /** + * Load the route files and register their REST routes. + * + * Deferred here (rest_api_init), not run from register() itself: register() + * runs on every request that boots this package, and on WPCOM Simple that's + * every request across all of WPCOM's public-api process, most of which + * never dispatch a Premium Analytics route. dashboard-layout.php hooks + * get_user_metadata unconditionally at file scope, and dashboard-sections.php + * hydrates its own section registry at file scope — neither should run + * before WordPress has decided this request is actually dispatching a REST + * route. widget-modules.php hydrates the widget type registry itself, lazily, + * only when its REST callback or the boot-deps filter actually runs. + * + * @return void + */ + public static function boot_routes() { require_once __DIR__ . '/widget-modules.php'; require_once __DIR__ . '/dashboard-layout.php'; require_once __DIR__ . '/dashboard-sections.php'; - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ); - add_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ); + register_widget_modules_rest_route(); + register_dashboard_default_layout_route(); + register_dashboard_sections_rest_routes(); } } diff --git a/projects/packages/premium-analytics/src/widget-modules.php b/projects/packages/premium-analytics/src/widget-modules.php index 0aee0d7c827e..1cb634305641 100644 --- a/projects/packages/premium-analytics/src/widget-modules.php +++ b/projects/packages/premium-analytics/src/widget-modules.php @@ -33,12 +33,45 @@ function register_widget_modules_rest_route() { ); } +/** + * Load and hydrate the widget type registry, once. + * + * Deferred to here (the registry's only two readers) instead of running + * unconditionally at boot: this package boots on every request to a site + * that has it active, and on WPCOM Simple this file's registration runs on + * every public-api request across all of WPCOM, not just ones that touch + * Premium Analytics. Most such requests never read the registry at all, so + * parsing the widget manifest and hydrating it eagerly was wasted work. + * + * @return void + */ +function ensure_widget_registry_ready() { + static $ready = false; + if ( $ready ) { + return; + } + $ready = true; + + require_once __DIR__ . '/widget-types.php'; + require_once __DIR__ . '/widget-availability.php'; + + // Manifest register_widget_types() reads; absent without a JS build. + $widgets_manifest = __DIR__ . '/../build/widgets.php'; + if ( file_exists( $widgets_manifest ) ) { + require_once $widgets_manifest; + } + + bootstrap_widget_types(); +} + /** * Build the REST response: one record per available widget type. * * @return \WP_REST_Response */ function get_widget_modules_response() { + ensure_widget_registry_ready(); + $records = array(); foreach ( get_available_widget_types() as $widget_type ) { @@ -61,6 +94,8 @@ function get_widget_modules_response() { * @return array Updated boot dependencies. */ function add_widget_modules_to_boot_deps( $boot_dependencies ) { + ensure_widget_registry_ready(); + foreach ( get_available_widget_types() as $widget_type ) { if ( ! empty( $widget_type->render_module ) ) { $boot_dependencies[] = array( diff --git a/projects/packages/premium-analytics/tests/php/Analytics_Test.php b/projects/packages/premium-analytics/tests/php/Analytics_Test.php index ae24f41ced8d..f2f7839759c9 100644 --- a/projects/packages/premium-analytics/tests/php/Analytics_Test.php +++ b/projects/packages/premium-analytics/tests/php/Analytics_Test.php @@ -24,6 +24,8 @@ class Analytics_Test extends TestCase { protected function tearDown(): void { unset( $_GET['page'] ); unset( $GLOBALS['current_screen'] ); + global $wp_rest_server; + $wp_rest_server = null; remove_all_actions( 'jetpack_sync_processed_actions' ); remove_all_actions( 'plugins_loaded' ); remove_all_actions( 'rest_api_init' ); @@ -78,15 +80,26 @@ public function test_init_skips_local_proxy_in_wpcom_simple_mode() { /** * Normal package bootstrap serves the dashboard support routes from the site. + * + * The route files themselves are loaded lazily, on rest_api_init, via + * Dashboard_Support_Routes::boot_routes() - so this checks that hook is + * wired, then dispatches it and confirms the routes actually land. */ public function test_init_registers_dashboard_support_routes_by_default() { $this->reset_analytics_init_state(); Analytics::init(); - $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ) ); - $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ) ); - $this->assertNotFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ) ); + $this->assertNotFalse( + has_action( 'rest_api_init', array( Dashboard_Support_Routes::class, 'boot_routes' ) ) + ); + + global $wp_rest_server; + $wp_rest_server = new \WP_REST_Server(); + do_action( 'rest_api_init' ); + + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wpcom/v2/widget-modules', $routes ); } /** @@ -97,9 +110,9 @@ public function test_init_skips_dashboard_support_routes_in_wpcom_simple_mode() Analytics::init_wpcom_simple(); - $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_widget_modules_rest_route' ) ); - $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_default_layout_route' ) ); - $this->assertFalse( has_action( 'rest_api_init', __NAMESPACE__ . '\\register_dashboard_sections_rest_routes' ) ); + $this->assertFalse( + has_action( 'rest_api_init', array( Dashboard_Support_Routes::class, 'boot_routes' ) ) + ); } /** diff --git a/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php b/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php index d603c588ae20..e413574f5407 100644 --- a/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php +++ b/projects/packages/premium-analytics/tests/php/Widget_Modules_Test.php @@ -47,4 +47,17 @@ public function test_widget_modules_route_uses_wpcom_v2_namespace() { $this->assertArrayHasKey( self::ROUTE, $routes ); $this->assertArrayNotHasKey( self::LEGACY_ROUTE, $routes ); } + + /** + * The route's callback works standalone: it hydrates the widget type + * registry itself (ensure_widget_registry_ready()) rather than assuming a + * caller already did, so it returns valid data even when nothing else in + * the process has touched the registry yet. + */ + public function test_response_hydrates_the_registry_on_first_use() { + $response = get_widget_modules_response(); + + $this->assertInstanceOf( \WP_REST_Response::class, $response ); + $this->assertIsArray( $response->get_data() ); + } }