-
Notifications
You must be signed in to change notification settings - Fork 107
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add site health check to detect blocked REST API and short-circuit optimization when unavailable #1762
Add site health check to detect blocked REST API and short-circuit optimization when unavailable #1762
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should there be a plugin activation hook added as well which does add_option()
for the new option and then also kicks off (or schedules) a REST API check? Ideally there would be a warning shown immediately after activating the plugin (e.g. on the plugins list table screen) whether the REST API is working so that the user doesn't have to discover it later via Site Health.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe put site-health
in the root directory instead of inside includes
since there are no other directories in there?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I thought in future if we added something like admin dashboard for managing URL metrics or any other admin dashboard related thing then it would be better to add that feature in includes/admin
. But we can just refactor things later when we need it so moving site-health
to root makes sence.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, let's put it in the root for now since all other directories are there.
if we added something like admin dashboard for managing URL metrics or any other admin dashboard related thing
Aside: I did put together a rough utility plugin for this: https://github.com/westonruter/od-admin-ui
'<p>%s</p>', | ||
esc_html__( 'The Optimization Detective endpoint could not be reached. This might mean the REST API is disabled or blocked.', 'optimization-detective' ) | ||
); | ||
update_option( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the first PR that adds an option to to Optimization Detective. We'll need to make sure that the relevant delete_option()
calls get added to the plugin's uninstall.php
.
// Disable detection if the REST API is disabled. | ||
$od_rest_api_info = get_option( 'od_rest_api_info' ); | ||
if ( is_array( $od_rest_api_info ) && isset( $od_rest_api_info['available'] ) ) { | ||
$needs_detection = (bool) $od_rest_api_info['available']; | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, this check wouldn't make sense here. It should rather be done in od_maybe_add_template_output_buffer_filter()
to short-circuit if the REST API it is not available.
update_option( | ||
'od_rest_api_info', | ||
array( | ||
'status' => 'error', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should the $error->get_message()
and maybe $error->get_code()
be stored here?
update_option( | ||
'od_rest_api_info', | ||
array( | ||
'status' => 'forbidden', |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of storing the string, what about storing the $status_code
instead?
'available' => false, | ||
) | ||
); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The else
condition should be added as an error result as well. Here especially the $status_code
could be used.
&& count( $expected_params ) === count( array_intersect( $data['data']['params'], $expected_params ) ) | ||
) { | ||
// The REST API endpoint is available. | ||
update_option( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of having update_option()
appearing in multiple places, each condition could populate an $info
variable which is then sent into update_option()
once at the end of the function.
wp_schedule_event( time(), 'hourly', 'od_rest_api_health_check_event' ); | ||
} | ||
} | ||
add_action( 'wp', 'od_schedule_rest_api_health_check' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Is this a best practice? Should it rather go in admin_init
to avoid frontend writes? I'm not sure what others do here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think scheduling on plugin activation hook will be better than admin_init
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The problem is that the plugin activation hook doesn't work when network-activating a plugin in multisite.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In looking at WP_Site_Health
, it goes ahead and schedules an event even for frontend requests, since it calls its maybe_create_scheduled_event
method in the constructor. And the instance is loaded in wp-settings.php
. Nevertheless, since a database write is involved, it is preferable if event scheduling happens via an admin request and not unauthenticated frontend requests.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please look at this comment regarding which hook should be used.
*/ | ||
function od_schedule_rest_api_health_check(): void { | ||
if ( ! (bool) wp_next_scheduled( 'od_rest_api_health_check_event' ) ) { | ||
wp_schedule_event( time(), 'hourly', 'od_rest_api_health_check_event' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think hourly is too much. Maybe weekly would make sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I currently have it set to run weekly, but that might be too infrequent. If a configuration change disables the REST API, it could take an entire week for user to detect the issue. I believe running it daily would be a better choice.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think weekly is fine. It's not likely that a user would be changing the availability of the REST API. If we check at the moment that a plugin is activated, and then check weekly thereafter, then this should be good. Note that Site Health's own checks run on a weekly basis via the wp_site_health_scheduled_check
action.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking forward to getting this out there!
add_action( | ||
'admin_notices', | ||
static function (): void { | ||
od_maybe_render_rest_api_health_check_admin_notice( false ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, the rest case should actually run the admin_notices
action to increase code coverage here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done in 6fee919
|| | ||
is_wp_error( $response ) | ||
) { | ||
return $response; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looks like I neglected to include a test for the cached state.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See 6fee919
$response = od_get_rest_api_health_check_response( false ); | ||
$result = od_compose_site_health_result( $response ); | ||
$is_unavailable = 'good' !== $result['status']; | ||
update_option( 'od_rest_api_unavailable', $is_unavailable ? '1' : '0' ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This intentionally makes the option autoloaded. Maybe we should pass the parameter to make this explicit.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See 8926f10
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there are still a few things in here that need to be addressed, given the major additional changes made.
$response = wp_remote_post( | ||
$rest_url, | ||
array( | ||
'headers' => array( 'Content-Type' => 'application/json' ), | ||
'sslverify' => false, | ||
) | ||
); | ||
|
||
// This transient will be used when showing the admin notice with the plugin on the plugins screen. | ||
// The 1-day expiration allows for fresher content than the weekly check initiated by Site Health. | ||
set_transient( $transient_key, $response, DAY_IN_SECONDS ); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm a bit concerned about trying to store a potential WP_Error
object in a transient. Can we parse that into a raw data array instead to make this safer?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is it concerning to store a WP_Error
in a transient? The object gets serialized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hmm, is there prior art for that in WordPress? I suppose it could work, but have you tested this, both using the DB for transients as well as a persistent object cache?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Looking at the WP_Error
class, for instance I would expect the $additional_data
property to be missing, since it's not public
. Granted, that data may not be important for what we're doing here, but it shows the fragility of relying on serialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As noted above, the test_od_get_rest_api_health_check_response
test confirms that transients return unserialized WP_Error
instances.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Take a look at fetch_feed()
such as used in the RSS widget. It stores an array that contains objects in it.
FWIW, I've been aware of storing class instances in transients and options since before I can remember. What's the difference with arrays? Both require serialization.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I was googling for other examples, and I found a reference to Site Kit storing objects in transients: https://wpengine.com/resources/guide-to-transients-in-wordpress/
I can't find the current reference to that code in GitHub though.
Here's an example in the Transients documentation about storing a WP_Query
instance in a transient: https://developer.wordpress.org/apis/transients/#complete-example
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's another example where an object is stored in a transient in wp_version_check()
: https://github.com/WordPress/wordpress-develop/blob/2f654881e494424634d5821d1ef37c06edb8923a/src/wp-includes/update.php#L36-L73
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Objects are also stored in object cache in WP_Term_Query
: https://github.com/WordPress/wordpress-develop/blob/2f654881e494424634d5821d1ef37c06edb8923a/src/wp-includes/class-wp-term-query.php#L879-L902
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay, you have me convinced. With this already done elsewhere in Core, this shouldn't be an issue, and the WP_Error
class is basically always available, so there shouldn't be a problem unserializing it. 👍
Co-authored-by: felixarntz <[email protected]>
Co-authored-by: felixarntz <[email protected]>
Co-authored-by: felixarntz <[email protected]>
93a51d8
to
02953c7
Compare
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@westonruter This looks good, only a few minor points now. My only blocking concern still is #1762 (comment) (I don't think we can rely on serialization for sites with a persistent object cache).
Co-authored-by: Felix Arntz <[email protected]>
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM, thanks for all the iterations and discussion @b1ink0 @westonruter!
Summary
Fixes #1731
Relevant technical choices
/optimization-detective/v1/url-metrics:store
REST API endpoint. The process will short-circuit if the endpoint is inaccessible.Scenarios:
When the health check passes
When the REST API endpoint returns a forbidden error
When the REST API endpoint returns an unauthorized error
When other errors are encountered