Skip to content
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

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
88952a5
Add REST API health check for Optimization Detective
b1ink0 Dec 19, 2024
4bff9fe
Improve error message of site health check
b1ink0 Dec 19, 2024
56d769a
Add scheduled health check for Optimization Detective REST API
b1ink0 Dec 19, 2024
ba911e1
Save site health check status for Optimization Detective REST API
b1ink0 Dec 19, 2024
12a229c
Add test for when REST API is available
b1ink0 Dec 19, 2024
a0f460d
Add tests for REST API unauthorized error handling
b1ink0 Dec 19, 2024
c29cb7c
Add test for REST API forbidden error handling
b1ink0 Dec 19, 2024
8d8ae3e
Refactor to move site-health directory to plugin root directory
b1ink0 Dec 20, 2024
422a5bf
Clear site health check data during plugin uninstallation
b1ink0 Dec 20, 2024
0bc74f7
Move REST API availability check to appropriate function
b1ink0 Dec 20, 2024
36042fd
Add error message and error code to option
b1ink0 Dec 20, 2024
ad21df8
Refactor to store status code instead of string, Add fallback else co…
b1ink0 Dec 20, 2024
0bba468
Refactor to only use update_option once
b1ink0 Dec 20, 2024
e1b9004
Add activation hook to initialize option value
b1ink0 Dec 23, 2024
dbede65
Move scheduling to activation hook
b1ink0 Dec 23, 2024
2b2ca3e
Change health check scheduling from hourly to weekly
b1ink0 Dec 23, 2024
e7c1001
Run REST API health check on plugin activation, Display notice if che…
b1ink0 Dec 24, 2024
7e4e057
Move activation logic to separate function
b1ink0 Dec 24, 2024
a2baa65
Refactor activation logic to directly call REST API health check func…
b1ink0 Dec 24, 2024
e1ec10c
Merge branch 'trunk' into add/site-health-check-for-od-rest-api
b1ink0 Dec 24, 2024
48c83a5
Improve site health status messages for clarity and consistency
b1ink0 Dec 25, 2024
8895d35
Refactor site health checks files by combining them into single file
b1ink0 Jan 8, 2025
2fbd530
Move added action and filters for site health checks to plugins's hoo…
b1ink0 Jan 8, 2025
5949bc9
Merge branch 'trunk' into add/site-health-check-for-od-rest-api
b1ink0 Jan 8, 2025
1aa82b4
Merge branch 'trunk' into add/site-health-check-for-od-rest-api
b1ink0 Jan 8, 2025
98fff4d
Merge branch 'trunk' into add/site-health-check-for-od-rest-api
westonruter Jan 8, 2025
c095436
Remove scheduled REST API health check functions and related action h…
b1ink0 Jan 13, 2025
ebcd804
Refactor to use admin_init instead of plugin activation hook, On plug…
b1ink0 Jan 13, 2025
57f7566
Show global admin notice on plugin activation and meta row notice on …
b1ink0 Jan 13, 2025
226584f
Update REST API endpoint messages to include 'Optimization Detective'…
b1ink0 Jan 14, 2025
1855ecb
Make params check simple, add default empty error_message, add check …
b1ink0 Jan 14, 2025
0ad417a
Improve REST API health check notices logic and message clarity
b1ink0 Jan 14, 2025
7ad36cd
Print REST API health check notice at admin_notices action without no…
westonruter Jan 15, 2025
d957745
Refactor construction of site health strings
westonruter Jan 15, 2025
4c1b2dc
Include Site Health information in admin notices
westonruter Jan 15, 2025
7f76d31
Add covers annotations
westonruter Jan 15, 2025
a0cce21
Only store inaccessible state in option and put response in transient
westonruter Jan 15, 2025
22dde1b
Remove redundant unscheduling of cron event
b1ink0 Jan 15, 2025
9869150
Refactor test to use data provider
westonruter Jan 15, 2025
55e3b55
Use 'unavailable' instead of 'inaccessible'
westonruter Jan 15, 2025
85b13f4
Omit generator tag when REST API is unavailable
westonruter Jan 15, 2025
51a56ba
Fix checking array shape
westonruter Jan 15, 2025
2a132ca
Amend generator with REST API availability
westonruter Jan 15, 2025
e22bb84
Rename functions to better reflect purpose
westonruter Jan 15, 2025
7eda72c
Add missing private tags to helper functions
westonruter Jan 15, 2025
cc16600
Pass through original HTTP message
westonruter Jan 15, 2025
523daaa
Improve styling of admin notice
westonruter Jan 15, 2025
a1335e0
Improve function names
westonruter Jan 15, 2025
e5d3f72
Account for another site_status_tests filter not returning an array
westonruter Jan 15, 2025
c6b4417
Add od_render_generator_meta_tag() test for when the REST API is unav…
westonruter Jan 15, 2025
3212dae
Add test for od_get_cache_purge_post_id()
westonruter Jan 15, 2025
19ff8e4
Merge branch 'trunk' of https://github.com/WordPress/performance into…
westonruter Jan 15, 2025
546217f
Update check in od_maybe_add_template_output_buffer_filter() and add …
westonruter Jan 15, 2025
3d5e8a8
Add test coverage for Site Health logic
westonruter Jan 16, 2025
c3b682f
Account for Site Health not being available in multisite to regular a…
westonruter Jan 16, 2025
c085ff0
Add test coverage for HTTP request returning WP_Error
westonruter Jan 16, 2025
50a1898
Fix absent site health test after failed function rename
westonruter Jan 16, 2025
c7d31a7
Further account for multisite and Site Health
westonruter Jan 16, 2025
85da162
Make Optimization Detective REST API access a critical error and impr…
westonruter Jan 16, 2025
0772ebe
Increase specificity of what the expected error response looks like
westonruter Jan 16, 2025
dbff1f7
Show REST API error message in blockquote if available; add Nginx err…
westonruter Jan 16, 2025
0693086
Add test to make sure hooks are added
b1ink0 Jan 16, 2025
8926f10
Explicitly autoload od_rest_api_unavailable option
westonruter Jan 16, 2025
6fee919
Add explicit test for od_get_rest_api_health_check_response
westonruter Jan 16, 2025
ddc6164
Remove checking for specific missing required params
westonruter Jan 17, 2025
b4736a4
Remove overkill type checking for transient response
westonruter Jan 17, 2025
02953c7
Change Site Health test from critical back to recommended
westonruter Jan 17, 2025
14e9471
Use definite article instead of possessive
westonruter Jan 17, 2025
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
15 changes: 14 additions & 1 deletion plugins/optimization-detective/helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
* Initializes extensions for Optimization Detective.
*
* @since 0.7.0
* @access private
*/
function od_initialize_extensions(): void {
/**
Expand All @@ -29,6 +30,9 @@ function od_initialize_extensions(): void {
/**
* Generates a media query for the provided minimum and maximum viewport widths.
*
* This helper function is available for extensions to leverage when manually printing STYLE rules via
* {@see OD_HTML_Tag_Processor::append_head_html()} or {@see OD_HTML_Tag_Processor::append_body_html()}
*
* @since 0.7.0
*
* @param int|null $minimum_viewport_width Minimum viewport width.
Expand Down Expand Up @@ -59,16 +63,25 @@ function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_vi
* See {@see 'wp_head'}.
*
* @since 0.1.0
* @access private
*/
function od_render_generator_meta_tag(): void {
// Use the plugin slug as it is immutable.
echo '<meta name="generator" content="optimization-detective ' . esc_attr( OPTIMIZATION_DETECTIVE_VERSION ) . '">' . "\n";
$content = 'optimization-detective ' . OPTIMIZATION_DETECTIVE_VERSION;

// Indicate that the plugin will not be doing anything because the REST API is unavailable.
if ( od_is_rest_api_unavailable() ) {
$content .= '; rest_api_unavailable';
}

echo '<meta name="generator" content="' . esc_attr( $content ) . '">' . "\n";
}

/**
* Gets the path to a script or stylesheet.
*
* @since 0.9.0
* @access private
*
* @param string $src_path Source path, relative to plugin root.
* @param string|null $min_path Minified path. If not supplied, then '.min' is injected before the file extension in the source path.
Expand Down
3 changes: 3 additions & 0 deletions plugins/optimization-detective/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,6 @@
OD_URL_Metrics_Post_Type::add_hooks();
add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' );
add_action( 'wp_head', 'od_render_generator_meta_tag' );
add_filter( 'site_status_tests', 'od_add_rest_api_availability_test' );
add_action( 'admin_init', 'od_maybe_run_rest_api_health_check' );
add_action( 'after_plugin_row_meta', 'od_render_rest_api_health_check_admin_notice_in_plugin_row', 30 );

Check warning on line 20 in plugins/optimization-detective/hooks.php

View check run for this annotation

Codecov / codecov/patch

plugins/optimization-detective/hooks.php#L18-L20

Added lines #L18 - L20 were not covered by tests
3 changes: 3 additions & 0 deletions plugins/optimization-detective/load.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,5 +127,8 @@

// Add hooks for the above requires.
require_once __DIR__ . '/hooks.php';

// Load site health checks.
require_once __DIR__ . '/site-health.php';

Check warning on line 132 in plugins/optimization-detective/load.php

View check run for this annotation

Codecov / codecov/patch

plugins/optimization-detective/load.php#L132

Added line #L132 was not covered by tests
}
);
7 changes: 6 additions & 1 deletion plugins/optimization-detective/optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,12 @@ static function ( string $output, ?int $phase ): string {
* @access private
*/
function od_maybe_add_template_output_buffer_filter(): void {
if ( ! od_can_optimize_response() || isset( $_GET['optimization_detective_disabled'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if (
! od_can_optimize_response() ||
od_is_rest_api_unavailable() ||
isset( $_GET['optimization_detective_disabled'] ) // phpcs:ignore WordPress.Security.NonceVerification.Recommended

) {
return;
}
$callback = 'od_optimize_template_output_buffer';
Expand Down
293 changes: 293 additions & 0 deletions plugins/optimization-detective/site-health.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,293 @@
<?php
/**
* Site Health checks.
*
* @package optimization-detective
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.

Check warning on line 10 in plugins/optimization-detective/site-health.php

View check run for this annotation

Codecov / codecov/patch

plugins/optimization-detective/site-health.php#L9-L10

Added lines #L9 - L10 were not covered by tests
}

/**
* Adds the Optimization Detective REST API check to site health tests.
*
* @since n.e.x.t
* @access private
*
* @param array{direct: array<string, array{label: string, test: string}>}|mixed $tests Site Health Tests.
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
*/
function od_add_rest_api_availability_test( $tests ): array {
if ( ! is_array( $tests ) ) {
$tests = array();
}
$tests['direct']['optimization_detective_rest_api'] = array(
'label' => __( 'Optimization Detective REST API Endpoint Availability', 'optimization-detective' ),
'test' => static function () {
// Note: A closure is used here to improve symbol discovery for the sake of potential refactoring.
return od_test_rest_api_availability();
},
);

return $tests;
}

/**
* Tests availability of the Optimization Detective REST API endpoint.
*
* @since n.e.x.t
* @access private
*
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function od_test_rest_api_availability(): array {
$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',
true // Intentionally autoloaded since used on every frontend request.
);
return $result;
}

/**
* Checks whether the Optimization Detective REST API endpoint is unavailable.
*
* This merely checks the database option what was previously computed in the Site Health test as done in {@see od_test_rest_api_availability()}.
* This is to avoid checking for REST API availability during a frontend request. Note that when the plugin is first
* installed, the 'od_rest_api_unavailable' option will not be in the database, as the check has not been performed
* yet. Once Site Health's weekly check happens or when a user accesses the admin so that the admin_init action fires,
* then at this point the check will be performed at {@see od_maybe_run_rest_api_health_check()}. In practice, this will
* happen immediately after the user activates a plugin since the user is redirected back to the plugin list table in
* the admin. The reason for storing the negative unavailable state as opposed to the positive available state is that
* when an option does not exist then `get_option()` returns `false` which is the same falsy value as the stored `'0'`.
*
* @since n.e.x.t
* @access private
*
* @return bool Whether unavailable.
*/
function od_is_rest_api_unavailable(): bool {
return 1 === (int) get_option( 'od_rest_api_unavailable', '0' );
}

/**
* Tests availability of the Optimization Detective REST API endpoint.
*
* @since n.e.x.t
* @access private
*
* @param array<string, mixed>|WP_Error $response REST API response.
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function od_compose_site_health_result( $response ): array {
$common_description_html = '<p>' . wp_kses(
sprintf(
/* translators: %s is the REST API endpoint */
__( 'To collect URL Metrics from visitors the REST API must be available to unauthenticated users. Specifically, visitors must be able to perform a <code>POST</code> request to the <code>%s</code> endpoint.', 'optimization-detective' ),
'/' . OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE
),
array( 'code' => array() )
) . '</p>';

$result = array(
'label' => __( 'The Optimization Detective REST API endpoint is available', 'optimization-detective' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Optimization Detective', 'optimization-detective' ),
'color' => 'blue',
),
'description' => $common_description_html . '<p><strong>' . esc_html__( 'This appears to be working properly.', 'optimization-detective' ) . '</strong></p>',
'actions' => '',
'test' => 'optimization_detective_rest_api',
);

$error_label = __( 'The Optimization Detective REST API endpoint is unavailable', 'optimization-detective' );
$error_description_html = '<p>' . esc_html__( 'You may have a plugin active or server configuration which restricts access to logged-in users. Unauthenticated access must be restored in order for Optimization Detective to work.', 'optimization-detective' ) . '</p>';

if ( is_wp_error( $response ) ) {
$result['status'] = 'recommended';
$result['label'] = $error_label;
$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
sprintf(
/* translators: %s is the error code */
__( 'The REST API responded with the error code <code>%s</code> and the following error message:', 'optimization-detective' ),
esc_html( (string) $response->get_error_code() )
),
array( 'code' => array() )
) . '</p><blockquote>' . esc_html( $response->get_error_message() ) . '</blockquote>';
} else {
$code = wp_remote_retrieve_response_code( $response );
$message = wp_remote_retrieve_response_message( $response );
$body = wp_remote_retrieve_body( $response );
$data = json_decode( $body, true );

$is_expected = (
400 === $code &&
isset( $data['code'], $data['data']['params'] ) &&
'rest_missing_callback_param' === $data['code'] &&
is_array( $data['data']['params'] ) &&
count( $data['data']['params'] ) > 0
);
if ( ! $is_expected ) {
$result['status'] = 'recommended';
if ( 401 === $code ) {
$result['label'] = __( 'The Optimization Detective REST API endpoint is unavailable to logged-out users', 'optimization-detective' );
} else {
$result['label'] = $error_label;
}
$result['description'] = $common_description_html . $error_description_html . '<p>' . wp_kses(
sprintf(
/* translators: %d is the HTTP status code, %s is the status header description */
__( 'The REST API returned with an HTTP status of <code>%1$d %2$s</code>.', 'optimization-detective' ),
$code,
esc_html( $message )
),
array( 'code' => array() )
) . '</p>';

if ( isset( $data['message'] ) && is_string( $data['message'] ) ) {
$result['description'] .= '<blockquote>' . esc_html( $data['message'] ) . '</blockquote>';
}

$result['description'] .= '<details><summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary><pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre></details>';
}
}
return $result;
}

/**
* Gets the response to an Optimization Detective REST API store request to confirm it is available to unauthenticated requests.
*
* @since n.e.x.t
* @access private
*
* @param bool $use_cached Whether to use a previous response cached in a transient.
* @return array{ response: array{ code: int, message: string }, body: string }|WP_Error Response.
*/
function od_get_rest_api_health_check_response( bool $use_cached ) {
$transient_key = 'od_rest_api_health_check_response';
$response = $use_cached ? get_transient( $transient_key ) : false;
if ( false !== $response ) {
return $response;
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

See 6fee919

}
$rest_url = get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE );
$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 );
Comment on lines +179 to +189
Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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?

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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.

Copy link
Member

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

Copy link
Member

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

Copy link
Member

Choose a reason for hiding this comment

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

Copy link
Member

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. 👍

return $response;
}

/**
* Renders an admin notice if the REST API health check fails.
*
* @since n.e.x.t
* @access private
*
* @param bool $in_plugin_row Whether the notice is to be printed in the plugin row.
*/
function od_maybe_render_rest_api_health_check_admin_notice( bool $in_plugin_row ): void {
if ( ! od_is_rest_api_unavailable() ) {
return;
}

$response = od_get_rest_api_health_check_response( true );
$result = od_compose_site_health_result( $response );
if ( 'good' === $result['status'] ) {
// There's a slight chance the DB option is stale in the initial if statement.
return;

Check warning on line 210 in plugins/optimization-detective/site-health.php

View check run for this annotation

Codecov / codecov/patch

plugins/optimization-detective/site-health.php#L210

Added line #L210 was not covered by tests
}

$message = sprintf(
$in_plugin_row
? '<summary style="margin: 0.5em 0">%s %s</summary>'
: '<p><strong>%s %s</strong></p>',
esc_html__( 'Warning:', 'optimization-detective' ),
esc_html( $result['label'] )
);

$message .= $result['description']; // This has already gone through Kses.

if ( current_user_can( 'view_site_health_checks' ) ) {
$site_health_message = wp_kses(
sprintf(
/* translators: %s is the URL to the Site Health admin screen */
__( 'Please visit <a href="%s">Site Health</a> to re-check this once you believe you have resolved the issue.', 'optimization-detective' ),
esc_url( admin_url( 'site-health.php' ) )
),
array( 'a' => array( 'href' => array() ) )
);
$message .= "<p><em>$site_health_message</em></p>";
}

if ( $in_plugin_row ) {
$message = "<details>$message</details>";
}

wp_admin_notice(
$message,
array(
'type' => 'warning',
'additional_classes' => $in_plugin_row ? array( 'inline', 'notice-alt' ) : array(),
'paragraph_wrap' => false,
)
);
}

/**
* Displays an admin notice on the plugin row if the REST API health check fails.
*
* @since n.e.x.t
* @access private
*
* @param string $plugin_file Plugin file.
*/
function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void {
if ( 'optimization-detective/load.php' !== $plugin_file ) { // TODO: What if a different plugin slug is used?
felixarntz marked this conversation as resolved.
Show resolved Hide resolved
return;
}
od_maybe_render_rest_api_health_check_admin_notice( true );
}

/**
* Runs the REST API health check if it hasn't been run yet.
*
* This happens at the `admin_init` action to avoid running the check on the frontend. This will run on the first admin
* page load after the plugin has been activated. This allows for this function to add an action at `admin_notices` so
* that an error message can be displayed after performing that plugin activation request. Note that a plugin activation
* hook cannot be used for this purpose due to not being compatible with multisite. While the site health notice is
* shown at the `admin_notices` action once, the notice will only be displayed inline with the plugin row thereafter
* via {@see od_render_rest_api_health_check_admin_notice_in_plugin_row()}.
*
* @since n.e.x.t
* @access private
*/
function od_maybe_run_rest_api_health_check(): void {
// If the option already exists, then the REST API health check has already been performed.
if ( false !== get_option( 'od_rest_api_unavailable' ) ) {
return;
}

// This will populate the od_rest_api_unavailable option so that the function won't execute on the next page load.
if ( 'good' !== od_test_rest_api_availability()['status'] ) {
// Show any notice in the main admin notices area for the first page load (e.g. after plugin activation).
add_action(
'admin_notices',
static function (): void {
od_maybe_render_rest_api_health_check_admin_notice( false );
Copy link
Member

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.

Copy link
Member

Choose a reason for hiding this comment

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

Done in 6fee919

}
);
}
}
2 changes: 2 additions & 0 deletions plugins/optimization-detective/storage/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
*
* @since 0.1.0
* @access private
*
* @see od_compose_site_health_result()
*/
function od_register_endpoint(): void {

Expand Down
Loading
Loading