From 687ee13eddb74220dbeceb2075295ad7f4bbe178 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Mon, 15 Jun 2026 13:02:10 +0800 Subject: [PATCH] feat(premium-analytics): expose initial_full_sync_finished on sync status response --- .../wooa7s-1542-expose-sync-milestone-live | 4 ++ .../src/Sync/class-sync-status-tracker.php | 45 ++++++++++++++- .../php/Sync/Sync_Status_Tracker_Test.php | 55 +++++++++++++++++++ 3 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1542-expose-sync-milestone-live diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1542-expose-sync-milestone-live b/projects/packages/premium-analytics/changelog/wooa7s-1542-expose-sync-milestone-live new file mode 100644 index 000000000000..01b55f524772 --- /dev/null +++ b/projects/packages/premium-analytics/changelog/wooa7s-1542-expose-sync-milestone-live @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Expose the analytics initial-full-sync milestone (initial_full_sync_finished) on Jetpack's sync status REST response so the dashboard can read it live on every poll, not just at page load. diff --git a/projects/packages/premium-analytics/src/Sync/class-sync-status-tracker.php b/projects/packages/premium-analytics/src/Sync/class-sync-status-tracker.php index 84498f12e807..e3728567d086 100644 --- a/projects/packages/premium-analytics/src/Sync/class-sync-status-tracker.php +++ b/projects/packages/premium-analytics/src/Sync/class-sync-status-tracker.php @@ -11,8 +11,9 @@ /** * Listens for the end of an analytics-relevant Jetpack full sync, persists a - * one-time milestone option, and exposes that milestone to the frontend via - * `JetpackScriptData.premium_analytics`. + * one-time milestone option, and exposes that milestone to the frontend both at + * page load (via `JetpackScriptData.premium_analytics`) and live on Jetpack's + * `/jetpack/v4/sync/status` REST response. * * Why this exists: the extracted dashboard needs to distinguish "first-time * sync, please wait" from "we have data, render it." Jetpack's existing @@ -51,7 +52,13 @@ class Sync_Status_Tracker { const MILESTONE_ACTION = 'jetpack_premium_analytics_initial_full_sync_finished'; /** - * Wire up the listener and the script-data filter. + * Jetpack core's sync-status REST route. We enrich this existing response with + * the milestone rather than registering a dedicated endpoint. + */ + const SYNC_STATUS_ROUTE = '/jetpack/v4/sync/status'; + + /** + * Wire up the listener, the script-data filter, and the sync-status enricher. * * Idempotent: safe to call more than once. * @@ -60,6 +67,38 @@ class Sync_Status_Tracker { public static function configure() { add_action( 'jetpack_sync_processed_actions', array( self::class, 'on_sync_processed_actions' ) ); add_filter( 'jetpack_admin_js_script_data', array( self::class, 'inject_script_data' ) ); + add_filter( 'rest_post_dispatch', array( self::class, 'enrich_sync_status_response' ), 10, 3 ); + } + + /** + * Append the milestone to Jetpack core's GET /jetpack/v4/sync/status response + * so the dashboard can read it on every poll, not just at page load. + * + * Page-load script-data ({@see inject_script_data()}) is a one-time snapshot; + * reading the milestone here keeps it live for in-session completion without a + * dedicated endpoint. Only the already-authorized, successful status payload is + * touched — other routes and error responses pass through untouched. + * + * @param mixed $response Result to send to the client. Usually a WP_REST_Response. + * @param mixed $server The REST server instance (unused). + * @param mixed $request The request used to generate the response. + * @return mixed + */ + public static function enrich_sync_status_response( $response, $server, $request ) { + if ( ! $request instanceof \WP_REST_Request + || self::SYNC_STATUS_ROUTE !== $request->get_route() + || ! $response instanceof \WP_REST_Response + || $response->is_error() ) { + return $response; + } + + $data = $response->get_data(); + if ( is_array( $data ) ) { + $data['initial_full_sync_finished'] = (int) get_option( self::INITIAL_FULL_SYNC_OPTION, 0 ); + $response->set_data( $data ); + } + + return $response; } /** diff --git a/projects/packages/premium-analytics/tests/php/Sync/Sync_Status_Tracker_Test.php b/projects/packages/premium-analytics/tests/php/Sync/Sync_Status_Tracker_Test.php index 54ec862aafac..77ae2a9821cd 100644 --- a/projects/packages/premium-analytics/tests/php/Sync/Sync_Status_Tracker_Test.php +++ b/projects/packages/premium-analytics/tests/php/Sync/Sync_Status_Tracker_Test.php @@ -183,8 +183,63 @@ public function test_configure_registers_hooks() { has_filter( 'jetpack_admin_js_script_data', array( Sync_Status_Tracker::class, 'inject_script_data' ) ), 'tracker should filter jetpack_admin_js_script_data' ); + $this->assertNotFalse( + has_filter( 'rest_post_dispatch', array( Sync_Status_Tracker::class, 'enrich_sync_status_response' ) ), + 'tracker should filter rest_post_dispatch to enrich sync status' + ); remove_action( 'jetpack_sync_processed_actions', array( Sync_Status_Tracker::class, 'on_sync_processed_actions' ) ); remove_filter( 'jetpack_admin_js_script_data', array( Sync_Status_Tracker::class, 'inject_script_data' ) ); + remove_filter( 'rest_post_dispatch', array( Sync_Status_Tracker::class, 'enrich_sync_status_response' ) ); + } + + public function test_enrich_adds_milestone_to_sync_status_response() { + update_option( Sync_Status_Tracker::INITIAL_FULL_SYNC_OPTION, 1730000123 ); + $request = new \WP_REST_Request( 'GET', Sync_Status_Tracker::SYNC_STATUS_ROUTE ); + $response = new \WP_REST_Response( array( 'started' => true ) ); + + $result = Sync_Status_Tracker::enrich_sync_status_response( $response, null, $request ); + + $data = $result->get_data(); + $this->assertSame( 1730000123, $data['initial_full_sync_finished'] ); + $this->assertTrue( $data['started'], 'preserves existing fields' ); + } + + public function test_enrich_reports_zero_when_milestone_unset() { + $request = new \WP_REST_Request( 'GET', Sync_Status_Tracker::SYNC_STATUS_ROUTE ); + $response = new \WP_REST_Response( array() ); + + $result = Sync_Status_Tracker::enrich_sync_status_response( $response, null, $request ); + + $this->assertSame( 0, $result->get_data()['initial_full_sync_finished'] ); + } + + public function test_enrich_ignores_other_routes() { + update_option( Sync_Status_Tracker::INITIAL_FULL_SYNC_OPTION, 1730000123 ); + $request = new \WP_REST_Request( 'POST', '/jetpack/v4/sync/full-sync' ); + $response = new \WP_REST_Response( array( 'scheduled' => true ) ); + + $result = Sync_Status_Tracker::enrich_sync_status_response( $response, null, $request ); + + $this->assertArrayNotHasKey( 'initial_full_sync_finished', $result->get_data() ); + } + + public function test_enrich_skips_error_responses() { + update_option( Sync_Status_Tracker::INITIAL_FULL_SYNC_OPTION, 1730000123 ); + $request = new \WP_REST_Request( 'GET', Sync_Status_Tracker::SYNC_STATUS_ROUTE ); + $response = new \WP_REST_Response( array( 'code' => 'forbidden' ), 403 ); + + $result = Sync_Status_Tracker::enrich_sync_status_response( $response, null, $request ); + + $this->assertArrayNotHasKey( 'initial_full_sync_finished', $result->get_data() ); + } + + public function test_enrich_passes_through_non_rest_response() { + $request = new \WP_REST_Request( 'GET', Sync_Status_Tracker::SYNC_STATUS_ROUTE ); + $passthrough = new \WP_Error( 'boom' ); + + $result = Sync_Status_Tracker::enrich_sync_status_response( $passthrough, null, $request ); + + $this->assertSame( $passthrough, $result ); } }