Skip to content

Conversation

@dognose24
Copy link
Contributor

@dognose24 dognose24 commented Dec 5, 2025

Fixes STATS-184.

This PR fixes an issue where the post stats cache could get stuck on invalid data or a WP_Error and never refresh. When corrupted or malformed cache entries exist in post meta, subsequent calls to fetch_post_stats() would always return that bad data instead of refetching from WPCOM.

With this change, invalid or error cache entries trigger a fresh remote fetch and a self-healing cache update, while still honoring the existing cache expiration mechanism for valid data.

Proposed changes:

  • Introduce the refresh_post_stats_cache() helper to handle the fetch from WPCOM + update post meta flow
  • Detect and recover from invalid cache entries in fetch_post_stats()

Other information:

  • Have you written new tests for your changes, if applicable?
  • Have you checked the E2E test CI results, and verified that your changes do not break them?
  • Have you tested your changes on WordPress.com, if applicable (if so, you'll see a generated comment below with a script to run)?

Jetpack product discussion

Does this pull request change what data or activity we track or use?

No.

Testing instructions:

  • Test on the case in the issue from the support ticket
  • Ensure the This individual post views count of Blog Stats in any post is correctly aligned with /stats/post/{post-id}/{site-slug}.

@github-actions
Copy link
Contributor

github-actions bot commented Dec 5, 2025

Are you an Automattician? Please test your changes on all WordPress.com environments to help mitigate accidental explosions.

  • To test on WoA, go to the Plugins menu on a WoA dev site. Click on the "Upload" button and follow the upgrade flow to be able to upload, install, and activate the Jetpack Beta plugin. Once the plugin is active, go to Jetpack > Jetpack Beta, select your plugin (Jetpack or WordPress.com Site Helper), and enable the update/blog_stats_debug branch.
  • To test on Simple, run the following command on your sandbox:
bin/jetpack-downloader test jetpack update/blog_stats_debug
bin/jetpack-downloader test jetpack-mu-wpcom-plugin update/blog_stats_debug

Interested in more tips and information?

  • In your local development environment, use the jetpack rsync command to sync your changes to a WoA dev blog.
  • Read more about our development workflow here: PCYsg-eg0-p2
  • Figure out when your changes will be shipped to customers here: PCYsg-eg5-p2

@github-actions
Copy link
Contributor

github-actions bot commented Dec 5, 2025

Thank you for your PR!

When contributing to Jetpack, we have a few suggestions that can help us test and review your patch:

  • ✅ Include a description of your PR changes.
  • ✅ Add a "[Status]" label (In Progress, Needs Review, ...).
  • ✅ Add a "[Type]" label (Bug, Enhancement, Janitorial, Task).
  • ✅ Add testing instructions.
  • ✅ Specify whether this PR includes any changes to data or privacy.
  • ✅ Add changelog entries to affected projects

This comment will be updated as you work on your PR and make changes. If you think that some of those checks are not needed for your PR, please explain why you think so. Thanks for cooperation 🤖


Follow this PR Review Process:

  1. Ensure all required checks appearing at the bottom of this PR are passing.
  2. Make sure to test your changes on all platforms that it applies to. You're responsible for the quality of the code you ship.
  3. You can use GitHub's Reviewers functionality to request a review.
  4. When it's reviewed and merged, you will be pinged in Slack to deploy the changes to WordPress.com simple once the build is done.

If you have questions about anything, reach out in #jetpack-developers for guidance!

@dognose24 dognose24 added the [Type] Bug When a feature is broken and / or not performing as intended label Dec 5, 2025
@jp-launch-control
Copy link

jp-launch-control bot commented Dec 5, 2025

Code Coverage Summary

This PR did not change code coverage!

That could be good or bad, depending on the situation. Everything covered before, and still is? Great! Nothing was covered before? Not so great. 🤷

Full summary · PHP report · JS report

@dognose24 dognose24 changed the title Debug missing Blog Stats post views Improve Post Stats cache handling for invalid or error data Dec 5, 2025
@dognose24 dognose24 requested a review from Copilot December 5, 2025 22:18
@dognose24 dognose24 added [Status] Needs Review This PR is ready for review. and removed [Status] In Progress labels Dec 5, 2025

This comment was marked as outdated.

@dognose24 dognose24 requested review from a team, jeherve and kangzj December 5, 2025 22:50
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 ) ) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I would lean to caching even if it's an error, so that we don't put too much pressure on the backend when something goes wrong. However, we need to make sure the data expires no matter it's an error or not. What do you think?

Copy link
Contributor Author

@dognose24 dognose24 Dec 9, 2025

Choose a reason for hiding this comment

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

Yeah, that's also what I thought about as an alternative. We can always respect the cache for 5 minutes, regardless of whether it was an error or invalid data. That means even if the data is incorrect, it would last at most 5 minutes, wouldn't it?

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 4ba2b0e.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

Comments suppressed due to low confidence (1)

projects/packages/stats/src/class-wpcom-stats.php:560

  • The new refresh_post_stats_cache() function and the cache recovery logic in fetch_post_stats() lack test coverage. The existing test suite has comprehensive tests for other stats methods (e.g., test_get_post_views, test_get_stats, etc.) but none for the post meta caching path.

Consider adding tests for:

  1. Cache recovery when malformed data exists in post meta
  2. Cache recovery when WP_Error is cached
  3. Cache recovery when non-array, non-time-keyed data exists
  4. The new refresh_post_stats_cache() function behavior
	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 );

			// 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
					);

					// If within cache period, return cached data regardless of validity.
					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, 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;
					}
				}
			}
		}

		// 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 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 );

		// 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;
	}

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


$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.
Comment on lines 532 to 533
// 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.

Comment on lines 522 to 530
// 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.

This comment was marked as outdated.

This comment was marked as outdated.

Co-authored-by: Copilot <[email protected]>
Copy link
Contributor

Copilot AI commented Dec 9, 2025

@dognose24 I've opened a new pull request, #46235, to work on those changes. Once the pull request is ready, I'll request review from you.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

@kangzj kangzj left a comment

Choose a reason for hiding this comment

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

Looks good and tests well! Thanks so much for all the efforts 👍

@dognose24 dognose24 merged commit 2982a5f into trunk Dec 9, 2025
71 checks passed
@dognose24 dognose24 deleted the update/blog_stats_debug branch December 9, 2025 13:24
@github-actions github-actions bot removed the [Status] Needs Review This PR is ready for review. label Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

[Block] Blog Stats [Package] Stats Data [Type] Bug When a feature is broken and / or not performing as intended

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants