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 7 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: 15 additions & 0 deletions plugins/optimization-detective/includes/site-health/load.php
Copy link
Member

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?

Copy link
Contributor Author

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.

Copy link
Member

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php
/**
* Site Health checks loader.
*
* @package optimization-detective
* @since n.e.x.t
*/

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

// REST API site health check.
require_once __DIR__ . '/rest-api/helper.php';
require_once __DIR__ . '/rest-api/hooks.php';
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
<?php
/**
* Helper functions for the Optimization Detective REST API health check.
*
* @package optimization-detective
* @since n.e.x.t
*/

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

/**
* Tests availability of the Optimization Detective REST API endpoint.
*
* @since n.e.x.t
*
* @return array{label: string, status: string, badge: array{label: string, color: string}, description: string, actions: string, test: string} Result.
*/
function od_optimization_detective_rest_api_test(): array {
$result = array(
'label' => __( 'Your site has functional Optimization Detective REST API endpoint', 'optimization-detective' ),
'status' => 'good',
'badge' => array(
'label' => __( 'Optimization Detective', 'optimization-detective' ),
'color' => 'blue',
),
'description' => sprintf(
'<p>%s</p>',
__( 'Optimization Detective can send and store URL metrics via REST API endpoint', 'optimization-detective' )
),
'actions' => '',
'test' => 'optimization_detective_rest_api',
);

$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,
)
);

if ( is_wp_error( $response ) ) {
$result['status'] = 'recommended';
$result['label'] = __( 'Your site encountered error accessing Optimization Detective REST API endpoint', 'optimization-detective' );
$result['description'] = sprintf(
'<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(
Copy link
Member

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.

'od_rest_api_info',
array(
'status' => 'error',
Copy link
Member

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?

'available' => false,
)
);
return $result;
}

$status_code = wp_remote_retrieve_response_code( $response );
$data = json_decode( wp_remote_retrieve_body( $response ), true );
$expected_params = array( 'slug', 'current_etag', 'hmac', 'url', 'viewport', 'elements' );
if (
400 === $status_code
&& isset( $data['data']['params'] )
&& is_array( $data['data']['params'] )
&& count( $expected_params ) === count( array_intersect( $data['data']['params'], $expected_params ) )
) {
// The REST API endpoint is available.
update_option(
Copy link
Member

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.

'od_rest_api_info',
array(
'status' => 'ok',
'available' => true,
)
);
return $result;
} elseif ( 401 === $status_code ) {
$result['status'] = 'recommended';
$result['label'] = __( 'Your site encountered unauthorized error for Optimization Detective REST API endpoint', 'optimization-detective' );
$result['description'] = sprintf(
'<p>%s</p>',
esc_html__( 'The REST API endpoint requires authentication. Ensure proper credentials are provided.', 'optimization-detective' )
);
update_option(
'od_rest_api_info',
array(
'status' => 'unauthorized',
'available' => false,
)
);
} elseif ( 403 === $status_code ) {
$result['status'] = 'recommended';
$result['label'] = __( 'Your site encountered forbidden error for Optimization Detective REST API endpoint', 'optimization-detective' );
$result['description'] = sprintf(
'<p>%s</p>',
esc_html__( 'The REST API endpoint is blocked check server or security settings.', 'optimization-detective' )
);
update_option(
'od_rest_api_info',
array(
'status' => 'forbidden',
Copy link
Member

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,
)
);
}
Copy link
Member

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.


return $result;
}

/**
* Periodically runs the Optimization Detective REST API health check.
*
* @since n.e.x.t
*/
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' );
}
}
add_action( 'wp', 'od_schedule_rest_api_health_check' );

/**
* Hook for the scheduled REST API health check.
*
* @since n.e.x.t
*/
function od_run_scheduled_rest_api_health_check(): void {
od_optimization_detective_rest_api_test();
}
add_action( 'od_rest_api_health_check_event', 'od_run_scheduled_rest_api_health_check' );
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php
/**
* Hook callbacks used for the Optimization Detective REST API health check.
*
* @package optimization-detective
* @since n.e.x.t
*/

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

/**
* Adds the Optimization Detective REST API check to site health tests.
*
* @since n.e.x.t
*
* @param array{direct: array<string, array{label: string, test: string}>} $tests Site Health Tests.
* @return array{direct: array<string, array{label: string, test: string}>} Amended tests.
*/
function od_optimization_detective_add_rest_api_test( array $tests ): array {
$tests['direct']['optimization_detective_rest_api'] = array(
'label' => __( 'Optimization Detective REST API Endpoint Availability', 'optimization-detective' ),
'test' => 'od_optimization_detective_rest_api_test',
);

return $tests;
}
add_filter( 'site_status_tests', 'od_optimization_detective_add_rest_api_test' );
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 @@ class_alias( OD_URL_Metric_Group_Collection::class, 'OD_URL_Metrics_Group_Collec

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

// Load site health checks.
require_once __DIR__ . '/includes/site-health/load.php';
}
);
6 changes: 6 additions & 0 deletions plugins/optimization-detective/optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,12 @@ function od_optimize_template_output_buffer( string $buffer ): string {
// Whether we need to add the data-od-xpath attribute to elements and whether the detection script should be injected.
$needs_detection = ! $group_collection->is_every_group_complete();

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

Copy link
Member

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.

do {
$tracked_in_url_metrics = false;
$processor->set_bookmark( $current_tag_bookmark ); // TODO: Should we break if this returns false?
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<?php
/**
* Tests for Optimization Detective REST API site health check.
*
* @package optimization-detective
*/

class Test_OD_REST_API_Site_Health_Check extends WP_UnitTestCase {

/**
* Holds mocked response headers for different test scenarios.
*
* @var array<string, array<string, mixed>>
*/
protected $mocked_responses = array();

/**
* Setup each test.
*/
public function setUp(): void {
parent::setUp();

// Clear any filters or mocks.
remove_all_filters( 'pre_http_request' );

// Add the filter to mock HTTP requests.
add_filter( 'pre_http_request', array( $this, 'mock_http_requests' ), 10, 3 );
}

/**
* Test that the site health check is `good` when the REST API is available.
*/
public function test_rest_api_available(): void {
$this->mocked_responses = array(
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response(
400,
'Bad Request',
array(
'data' => array(
'params' => array( 'slug', 'current_etag', 'hmac', 'url', 'viewport', 'elements' ),
),
)
),
);

$result = od_optimization_detective_rest_api_test();
$od_rest_api_info = get_option( 'od_rest_api_info', array() );

$this->assertSame( 'good', $result['status'] );
$this->assertSame( 'ok', isset( $od_rest_api_info['status'] ) ? $od_rest_api_info['status'] : '' );
$this->assertTrue( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : false );
}

/**
* Test behavior when REST API returns an unauthorized error.
*/
public function test_rest_api_unauthorized(): void {
$this->mocked_responses = array(
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response(
401,
'Unauthorized'
),
);

$result = od_optimization_detective_rest_api_test();
$od_rest_api_info = get_option( 'od_rest_api_info', array() );

$this->assertSame( 'recommended', $result['status'] );
$this->assertSame( 'unauthorized', isset( $od_rest_api_info['status'] ) ? $od_rest_api_info['status'] : '' );
$this->assertFalse( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : true );
}

/**
* Test behavior when REST API returns an forbidden error.
*/
public function test_rest_api_forbidden(): void {
$this->mocked_responses = array(
get_rest_url( null, OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ) => $this->build_mock_response(
403,
'Forbidden'
),
);

$result = od_optimization_detective_rest_api_test();
$od_rest_api_info = get_option( 'od_rest_api_info', array() );

$this->assertSame( 'recommended', $result['status'] );
$this->assertSame( 'forbidden', isset( $od_rest_api_info['status'] ) ? $od_rest_api_info['status'] : '' );
$this->assertFalse( isset( $od_rest_api_info['available'] ) ? $od_rest_api_info['available'] : true );
}

/**
* Mock HTTP requests for assets to simulate different responses.
*
* @param bool $response A preemptive return value of an HTTP request. Default false.
* @param array<string, mixed> $args Request arguments.
* @param string $url The request URL.
* @return array<string, mixed> Mocked response.
*/
public function mock_http_requests( bool $response, array $args, string $url ): array {
if ( isset( $this->mocked_responses[ $url ] ) ) {
return $this->mocked_responses[ $url ];
}

// If no specific mock set, default to a generic success response.
return array(
'response' => array(
'code' => 200,
'message' => 'OK',
),
);
}

/**
* Build a mock response.
*
* @param int $status_code HTTP status code.
* @param string $message HTTP status message.
* @param array<string, mixed> $body Response body.
* @return array<string, mixed> Mocked response.
*/
protected function build_mock_response( int $status_code, string $message, array $body = array() ): array {
return array(
'response' => array(
'code' => $status_code,
'message' => $message,
),
'body' => wp_json_encode( $body ),
);
}
}
Loading