Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions projects/packages/stats/changelog/update-blog_stats_debug
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Improve Post Stats cache handling for invalid or error data
75 changes: 52 additions & 23 deletions projects/packages/stats/src/class-wpcom-stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Comment on lines +559 to 565
Copy link

Copilot AI Dec 9, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new refresh_post_stats_cache() method and the modified cache validation logic in fetch_post_stats() lack test coverage. Given that this package has comprehensive test coverage (as evidenced by WPCOM_Stats_Test.php with 863 lines of tests), the following scenarios should be tested:

  1. Invalid/malformed cache entries trigger a refresh
  2. WP_Error cached entries are handled correctly within and beyond expiration
  3. The new refresh_post_stats_cache() method properly caches both success and error responses
  4. Cache expiration logic works correctly with the new validation

This is critical since the PR specifically addresses a bug where "post stats cache could get stuck on invalid data or a WP_Error."

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@copilot open a new pull request to apply changes based on this feedback

Expand Down
Loading