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..2b57c81d1e3cc --- /dev/null +++ b/projects/packages/stats/changelog/update-blog_stats_debug @@ -0,0 +1,4 @@ +Significance: patch +Type: changed + +Improve Post Stats cache handling for invalid or error data diff --git a/projects/packages/stats/src/class-wpcom-stats.php b/projects/packages/stats/src/class-wpcom-stats.php index dd627f3907050..f52b4b971034c 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 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. @@ -503,34 +507,59 @@ 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 $data; - } - - $time = key( $data ); - $views = $data[ $time ] ?? null; - - // Bail if data is malformed. - if ( ! is_numeric( $time ) || ! is_array( $views ) ) { - return $data; - } + // Check if we have a valid cache structure with a time key. + if ( is_array( $data ) && ! empty( $data ) ) { + $time = key( $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 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 + ); - if ( ( time() - $time ) < $expiration ) { - return array_merge( array( 'cached_at' => $time ), $views ); + // If within cache period, return cached data after type validation. + if ( ( time() - $time ) < $expiration ) { + $cached_value = $data[ $time ]; + + // 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. + // Fall through to refresh. + } + } } } + // Cache doesn't exist, is expired, or is malformed - refresh it. + return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name ); + } + + /** + * 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. + * @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 ); + + // 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;