From e0ceb39bf6647b2a8c446388a8b7a4ccbc7d4a78 Mon Sep 17 00:00:00 2001 From: Dognose Date: Sat, 6 Dec 2025 04:33:07 +0800 Subject: [PATCH 1/9] Remove post stats cache --- .../packages/stats/src/class-wpcom-stats.php | 35 ++----------------- 1 file changed, 2 insertions(+), 33 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index dd627f3907050..26183c2b29743 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -496,39 +496,8 @@ protected function fetch_stats( $args = array() ) { * @return array|WP_Error */ protected function fetch_post_stats( $args, $post_id ) { - $endpoint = $this->build_endpoint(); - $meta_name = '_' . self::STATS_CACHE_TRANSIENT_PREFIX; - $stats_cache = get_post_meta( $post_id, $meta_name, false ); - - if ( $stats_cache ) { - $data = reset( $stats_cache ); - - if ( - ! is_array( $data ) - || empty( $data ) - || is_wp_error( $data ) - ) { - return $data; - } - - $time = key( $data ); - $views = $data[ $time ] ?? null; - - // Bail if data is malformed. - if ( ! is_numeric( $time ) || ! is_array( $views ) ) { - return $data; - } - - /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */ - $expiration = apply_filters( - 'jetpack_fetch_stats_cache_expiration', - self::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS - ); - - if ( ( time() - $time ) < $expiration ) { - return array_merge( array( 'cached_at' => $time ), $views ); - } - } + $endpoint = $this->build_endpoint(); + $meta_name = '_' . self::STATS_CACHE_TRANSIENT_PREFIX; $wpcom_stats = $this->fetch_remote_stats( $endpoint, $args ); update_post_meta( $post_id, $meta_name, array( time() => $wpcom_stats ) ); From 88770387ebc2bfb2f5de25c3e7dd2a22182a7640 Mon Sep 17 00:00:00 2001 From: Dognose Date: Sat, 6 Dec 2025 04:43:07 +0800 Subject: [PATCH 2/9] changelog --- projects/packages/stats/changelog/update-blog_stats_debug | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/stats/changelog/update-blog_stats_debug diff --git a/projects/packages/stats/changelog/update-blog_stats_debug b/projects/packages/stats/changelog/update-blog_stats_debug new file mode 100644 index 0000000000000..2af2fa7321b1e --- /dev/null +++ b/projects/packages/stats/changelog/update-blog_stats_debug @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Debug blog stats method From a73646533468ea53c2eabfb58bf0c58cbcbebd6c Mon Sep 17 00:00:00 2001 From: Dognose Date: Sat, 6 Dec 2025 05:54:53 +0800 Subject: [PATCH 3/9] Improve post stats cache handling for invalid or error data --- .../packages/stats/src/class-wpcom-stats.php | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 26183c2b29743..1674b7bd867de 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -496,11 +496,60 @@ protected function fetch_stats( $args = array() ) { * @return array|WP_Error */ protected function fetch_post_stats( $args, $post_id ) { - $endpoint = $this->build_endpoint(); - $meta_name = '_' . self::STATS_CACHE_TRANSIENT_PREFIX; + $endpoint = $this->build_endpoint(); + $meta_name = '_' . self::STATS_CACHE_TRANSIENT_PREFIX; + $stats_cache = get_post_meta( $post_id, $meta_name, false ); + if ( $stats_cache ) { + $data = reset( $stats_cache ); + + if ( + ! is_array( $data ) + || empty( $data ) + || is_wp_error( $data ) + ) { + return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); + } + + $time = key( $data ); + $views = $data[ $time ] ?? null; + + // Bail if data is malformed. + if ( ! is_numeric( $time ) || ! is_array( $views ) ) { + return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); + } + + /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */ + $expiration = apply_filters( + 'jetpack_fetch_stats_cache_expiration', + self::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS + ); + + if ( ( time() - $time ) < $expiration ) { + return array_merge( array( 'cached_at' => $time ), $views ); + } + } + + return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); + } + + /** + * Force fetch stats from WPCOM, and update cache if needed. + * + * @param string $endpoint The stats endpoint. + * @param array $args The query arguments. + * @param int $post_id The post ID. + * @param string $meta_name The meta name. + * + * @return array|WP_Error + */ + protected function refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ) { $wpcom_stats = $this->fetch_remote_stats( $endpoint, $args ); - update_post_meta( $post_id, $meta_name, array( time() => $wpcom_stats ) ); + + // Don't write error or empty results to cache. + if ( ! is_wp_error( $wpcom_stats ) && ! empty( $wpcom_stats ) ) { + update_post_meta( $post_id, $meta_name, array( time() => $wpcom_stats ) ); + } return $wpcom_stats; } From ffe8c0c7931865c30eadabf8e072320cacccebb6 Mon Sep 17 00:00:00 2001 From: Dognose Date: Sat, 6 Dec 2025 06:30:14 +0800 Subject: [PATCH 4/9] Update changelog --- projects/packages/stats/changelog/update-blog_stats_debug | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/projects/packages/stats/changelog/update-blog_stats_debug b/projects/packages/stats/changelog/update-blog_stats_debug index 2af2fa7321b1e..2b57c81d1e3cc 100644 --- a/projects/packages/stats/changelog/update-blog_stats_debug +++ b/projects/packages/stats/changelog/update-blog_stats_debug @@ -1,4 +1,4 @@ Significance: patch Type: changed -Debug blog stats method +Improve Post Stats cache handling for invalid or error data From 4ba2b0e9c52cd9de70ba2ea363d6804bc256073f Mon Sep 17 00:00:00 2001 From: Dognose Date: Tue, 9 Dec 2025 11:35:53 +0800 Subject: [PATCH 5/9] Respect the cache first and return errors or invalid data as they were --- .../packages/stats/src/class-wpcom-stats.php | 53 ++++++++++--------- 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 1674b7bd867de..30fd74044210a 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -503,33 +503,40 @@ protected function fetch_post_stats( $args, $post_id ) { if ( $stats_cache ) { $data = reset( $stats_cache ); - if ( - ! is_array( $data ) - || empty( $data ) - || is_wp_error( $data ) - ) { - return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); - } + // Check if we have a valid cache structure with a time key. + if ( is_array( $data ) && ! empty( $data ) ) { + $time = key( $data ); + + // If we have a numeric time, check if cache is still valid. + if ( is_numeric( $time ) ) { + /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */ + $expiration = apply_filters( + 'jetpack_fetch_stats_cache_expiration', + self::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS + ); - $time = key( $data ); - $views = $data[ $time ] ?? null; + // If within cache period, return cached data regardless of validity. + if ( ( time() - $time ) < $expiration ) { + $cached_value = $data[ $time ]; - // Bail if data is malformed. - if ( ! is_numeric( $time ) || ! is_array( $views ) ) { - return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); - } + // If it's a WP_Error, return it directly. + if ( is_wp_error( $cached_value ) ) { + return $cached_value; + } - /** This filter is already documented in projects/packages/stats/src/class-wpcom-stats.php */ - $expiration = apply_filters( - 'jetpack_fetch_stats_cache_expiration', - self::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS - ); + // If it's an array, merge with cached_at timestamp. + if ( is_array( $cached_value ) ) { + return array_merge( array( 'cached_at' => $time ), $cached_value ); + } - if ( ( time() - $time ) < $expiration ) { - return array_merge( array( 'cached_at' => $time ), $views ); + // For any other type, return as-is. + return $cached_value; + } + } } } + // Cache doesn't exist, is expired, or is malformed - refresh it. return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); } @@ -546,10 +553,8 @@ protected function fetch_post_stats( $args, $post_id ) { protected function refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ) { $wpcom_stats = $this->fetch_remote_stats( $endpoint, $args ); - // Don't write error or empty results to cache. - if ( ! is_wp_error( $wpcom_stats ) && ! empty( $wpcom_stats ) ) { - update_post_meta( $post_id, $meta_name, array( time() => $wpcom_stats ) ); - } + // Always cache the result, even if it's an error or empty. + update_post_meta( $post_id, $meta_name, array( time() => $wpcom_stats ) ); return $wpcom_stats; } From 68d3fe3369c11c3436133b10cef1284714ec0794 Mon Sep 17 00:00:00 2001 From: Dognose Date: Tue, 9 Dec 2025 12:31:51 +0800 Subject: [PATCH 6/9] Keep WP errors from cache along with the valid response --- .../packages/stats/src/class-wpcom-stats.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 30fd74044210a..199ab350bbb5d 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -515,22 +515,17 @@ protected function fetch_post_stats( $args, $post_id ) { self::STATS_CACHE_EXPIRATION_IN_MINUTES * MINUTE_IN_SECONDS ); - // If within cache period, return cached data regardless of validity. + // If within cache period, return cached data after type validation. if ( ( time() - $time ) < $expiration ) { $cached_value = $data[ $time ]; - // If it's a WP_Error, return it directly. - if ( is_wp_error( $cached_value ) ) { - return $cached_value; + // If it's an array or WP_Error, add cached time and return to user. + if ( is_array( $cached_value ) || is_wp_error( $cached_value ) ) { + return array_merge( array( 'cached_at' => $time ), (array) $cached_value ); } - // If it's an array, merge with cached_at timestamp. - if ( is_array( $cached_value ) ) { - return array_merge( array( 'cached_at' => $time ), $cached_value ); - } - - // For any other type, return as-is. - return $cached_value; + // For any other unexpected type, treat as malformed cache. + // Fall through to refresh. } } } From 3bbe021a46d458bf12eebdeaebd8ced2f133f1d8 Mon Sep 17 00:00:00 2001 From: Dognose Date: Tue, 9 Dec 2025 13:00:25 +0800 Subject: [PATCH 7/9] Update annotations --- projects/packages/stats/src/class-wpcom-stats.php | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 199ab350bbb5d..2f61ba896fca0 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -488,7 +488,11 @@ protected function fetch_stats( $args = array() ) { * * Unlike the above function, this caches data in the post meta table. As such, * it prevents wp_options from blowing up when retrieving views for large numbers - * of posts at the same time. However, the final response is the same as above. + * of posts at the same time. + * + * This function prioritizes cache time over data validity. If cached data exists + * and is within the expiration period, it will be returned even if it's a WP_Error + * or contains invalid data. This reduces API calls when remote fetch fails. * * @param array $args Query parameters. * @param int $post_id Post ID to acquire stats for. @@ -536,7 +540,11 @@ protected function fetch_post_stats( $args, $post_id ) { } /** - * Force fetch stats from WPCOM, and update cache if needed. + * Force fetch stats from WPCOM, and always update cache. + * + * This function will cache the result regardless of whether the fetch succeeds + * or fails. This ensures that failed requests are also cached, reducing the + * frequency of API calls when the remote service is experiencing issues. * * @param string $endpoint The stats endpoint. * @param array $args The query arguments. From 5b9fc39ce8fc06a1d0ad621125e4f752626433f9 Mon Sep 17 00:00:00 2001 From: Dognose Date: Tue, 9 Dec 2025 13:44:53 +0800 Subject: [PATCH 8/9] Update annotations Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- projects/packages/stats/src/class-wpcom-stats.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 2f61ba896fca0..1a1302a18e56c 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -490,9 +490,9 @@ protected function fetch_stats( $args = array() ) { * it prevents wp_options from blowing up when retrieving views for large numbers * of posts at the same time. * - * This function prioritizes cache time over data validity. If cached data exists - * and is within the expiration period, it will be returned even if it's a WP_Error - * or contains invalid data. This reduces API calls when remote fetch fails. + * This function returns valid arrays and WP_Error objects from cache if within the expiration period. + * If the cached entry is malformed or invalid, a refresh is triggered regardless of cache time. + * This self-healing behavior reduces API calls when remote fetch fails, but ensures data validity. * * @param array $args Query parameters. * @param int $post_id Post ID to acquire stats for. From bd5a7b129326e625e540e5f2ba3e2a095d0c3905 Mon Sep 17 00:00:00 2001 From: Jasper Kang Date: Tue, 9 Dec 2025 21:52:21 +1300 Subject: [PATCH 9/9] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- projects/packages/stats/src/class-wpcom-stats.php | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index 1a1302a18e56c..f52b4b971034c 100644 --- a/projects/packages/stats/src/class-wpcom-stats.php +++ b/projects/packages/stats/src/class-wpcom-stats.php @@ -523,9 +523,12 @@ protected function fetch_post_stats( $args, $post_id ) { if ( ( time() - $time ) < $expiration ) { $cached_value = $data[ $time ]; - // If it's an array or WP_Error, add cached time and return to user. - if ( is_array( $cached_value ) || is_wp_error( $cached_value ) ) { - return array_merge( array( 'cached_at' => $time ), (array) $cached_value ); + // If it's an array or WP_Error, handle appropriately. + if ( is_wp_error( $cached_value ) ) { + return $cached_value; + } + if ( is_array( $cached_value ) ) { + return array_merge( array( 'cached_at' => $time ), $cached_value ); } // For any other unexpected type, treat as malformed cache.