Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 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
68 changes: 47 additions & 21 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,56 @@ 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;
}
// Check if we have a valid cache structure with a time key.
if ( is_array( $data ) && ! empty( $data ) ) {
$time = key( $data );

$time = key( $data );
$views = $data[ $time ] ?? null;
// 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
);

// Bail if data is malformed.
if ( ! is_numeric( $time ) || ! is_array( $views ) ) {
return $data;
}
// If within cache period, return cached data after type validation.
if ( ( time() - $time ) < $expiration ) {
$cached_value = $data[ $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 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 ( ( time() - $time ) < $expiration ) {
return array_merge( array( 'cached_at' => $time ), $views );
// 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