Skip to content
Prev Previous commit
Next Next commit
Respect the cache first and return errors or invalid data as they were
  • Loading branch information
dognose24 committed Dec 9, 2025
commit 4ba2b0e9c52cd9de70ba2ea363d6804bc256073f
53 changes: 29 additions & 24 deletions projects/packages/stats/src/class-wpcom-stats.php
Original file line number Diff line number Diff line change
Expand Up @@ -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.
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 comment "return cached data regardless of validity" is misleading. The code immediately validates the cached value by checking if it's a WP_Error or array (lines 522-530). Consider updating the comment to: "If within cache period, return cached data after type validation."

Suggested change
// If within cache period, return cached data regardless of validity.
// If within cache period, return cached data after type validation.

Copilot uses AI. Check for mistakes.
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 );
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I guess we don't have to tell them apart - for those two cases we could both return:

return array_merge( array( 'cached_at' => $time ), $cached_value );

Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
// 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 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 );
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 68d3fe3.


if ( ( time() - $time ) < $expiration ) {
return array_merge( array( 'cached_at' => $time ), $views );
// For any other type, return as-is.
return $cached_value;
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.

Lines 532-533 will return any non-array, non-WP_Error cached value as-is, which could perpetuate invalid cached data (e.g., null, string, boolean) for the entire cache period. This contradicts the PR's goal of "detecting and recovering from invalid cache entries."

Consider either:

  1. Only returning the cached value if it's an array or WP_Error, falling through to refresh otherwise
  2. Adding explicit validation that cached_value is an expected type

Example:

// For any other unexpected type, treat as malformed cache.
// Fall through to refresh.

Then remove the return statement and let execution continue to line 540.

Suggested change
// For any other type, return as-is.
return $cached_value;
// For any other unexpected type, treat as malformed cache.
// Fall through to refresh.

Copilot uses AI. Check for mistakes.
Copy link
Contributor

Choose a reason for hiding this comment

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

Agree with Copilot here - other cases should all fall to malformed types?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

other cases should all fall to malformed types?

Looks reasonable to me, like only leave expected WP_Error thrown from cache.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Updated in 68d3fe3.

}
}
}
}

// Cache doesn't exist, is expired, or is malformed - refresh it.
return $this->refresh_post_stats_cache( $endpoint, $args, $post_id, $meta_name );
}

Expand All @@ -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;
}
Expand Down
Loading