-
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
Changes from 7 commits
88952a5
4bff9fe
56d769a
ba911e1
12a229c
a0f460d
c29cb7c
8d8ae3e
422a5bf
0bc74f7
36042fd
ad21df8
0bba468
e1b9004
dbede65
2b2ca3e
e7c1001
7e4e057
a2baa65
e1ec10c
48c83a5
8895d35
2fbd530
5949bc9
1aa82b4
98fff4d
c095436
ebcd804
57f7566
226584f
1855ecb
0ad417a
7ad36cd
d957745
4c1b2dc
7f76d31
a0cce21
22dde1b
9869150
55e3b55
85b13f4
51a56ba
2a132ca
e22bb84
7eda72c
cc16600
523daaa
a1335e0
e5d3f72
c6b4417
3212dae
19ff8e4
546217f
3d5e8a8
c3b682f
c085ff0
50a1898
c7d31a7
85da162
0772ebe
dbff1f7
0693086
8926f10
6fee919
ddc6164
b4736a4
02953c7
14e9471
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
'od_rest_api_info', | ||
array( | ||
'status' => 'error', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should the |
||
'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( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of having |
||
'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', | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Instead of storing the string, what about storing the |
||
'available' => false, | ||
) | ||
); | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
|
||
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' ); |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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']; | ||
} | ||
|
||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
||
do { | ||
$tracked_in_url_metrics = false; | ||
$processor->set_bookmark( $current_tag_bookmark ); // TODO: Should we break if this returns false? | ||
|
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 ), | ||
); | ||
} | ||
} |
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 insideincludes
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 movingsite-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.
Aside: I did put together a rough utility plugin for this: https://github.com/westonruter/od-admin-ui