Skip to content

Commit

Permalink
Include queried object in URL Metric to clean cache when stored
Browse files Browse the repository at this point in the history
  • Loading branch information
westonruter committed Nov 10, 2024
1 parent 537fa3b commit 619ac59
Show file tree
Hide file tree
Showing 11 changed files with 236 additions and 56 deletions.
1 change: 1 addition & 0 deletions plugins/embed-optimizer/tests/test-hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ public function test_embed_optimizer_add_hooks(): void {
$this->assertSame( 10, has_action( 'od_init', 'embed_optimizer_init_optimization_detective' ) );
$this->assertSame( 10, has_action( 'wp_head', 'embed_optimizer_render_generator' ) );
$this->assertSame( 10, has_action( 'wp_loaded', 'embed_optimizer_add_non_optimization_detective_hooks' ) );
$this->assertSame( 10, has_action( 'od_url_metric_stored', 'od_clean_queried_object_cache_for_stored_url_metric' ) );
}

/**
Expand Down
101 changes: 67 additions & 34 deletions plugins/optimization-detective/class-od-url-metric.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,35 +14,40 @@
/**
* Representation of the measurements taken from a single client's visit to a specific URL.
*
* @phpstan-type ViewportRect array{
* width: int,
* height: int
* }
* @phpstan-type DOMRect array{
* width: float,
* height: float,
* x: float,
* y: float,
* top: float,
* right: float,
* bottom: float,
* left: float
* }
* @phpstan-type ElementData array{
* isLCP: bool,
* isLCPCandidate: bool,
* xpath: non-empty-string,
* intersectionRatio: float,
* intersectionRect: DOMRect,
* boundingClientRect: DOMRect,
* }
* @phpstan-type Data array{
* uuid: non-empty-string,
* url: non-empty-string,
* timestamp: float,
* viewport: ViewportRect,
* elements: ElementData[]
* }
* @phpstan-type ViewportRect array{
* width: int,
* height: int
* }
* @phpstan-type QueriedObject array{
* type: 'post'|'user'|'term',
* id: int
* }
* @phpstan-type DOMRect array{
* width: float,
* height: float,
* x: float,
* y: float,
* top: float,
* right: float,
* bottom: float,
* left: float
* }
* @phpstan-type ElementData array{
* isLCP: bool,
* isLCPCandidate: bool,
* xpath: non-empty-string,
* intersectionRatio: float,
* intersectionRect: DOMRect,
* boundingClientRect: DOMRect,
* }
* @phpstan-type Data array{
* uuid: non-empty-string,
* url: non-empty-string,
* queriedObject: null|QueriedObject,
* timestamp: float,
* viewport: ViewportRect,
* elements: ElementData[]
* }
*
* @since 0.1.0
* @access private
Expand Down Expand Up @@ -199,21 +204,40 @@ public static function get_json_schema(): array {
'type' => 'object',
'required' => true,
'properties' => array(
'uuid' => array(
'uuid' => array(
'description' => __( 'The UUID for the URL Metric.', 'optimization-detective' ),
'type' => 'string',
'format' => 'uuid',
'required' => true,
'readonly' => true, // Omit from REST API.
),
'url' => array(
'url' => array(
'description' => __( 'The URL for which the metric was obtained.', 'optimization-detective' ),
'type' => 'string',
'required' => true,
'format' => 'uri',
'pattern' => '^https?://',
),
'viewport' => array(
'queriedObject' => array(
'type' => 'object',
'required' => false, // Not required since a query like is_home() will not have any queried object.
'properties' => array(
'type' => array(
'type' => array( 'string' ),
'description' => __( 'Queried object type.', 'optimization-detective' ),
'required' => true,
'enum' => array( 'post', 'term', 'user' ), // TODO: Should post_type be supported? There is no ID in this case, but the post type slug is used. But we don't need it to flush the cache.
),
'id' => array(
'type' => 'integer',
'description' => __( 'Queried object ID.', 'optimization-detective' ),
'required' => true,
'minimum' => 1,
),
),
'additionalProperties' => false,
),
'viewport' => array(
'description' => __( 'Viewport dimensions', 'optimization-detective' ),
'type' => 'object',
'required' => true,
Expand All @@ -231,14 +255,14 @@ public static function get_json_schema(): array {
),
'additionalProperties' => false,
),
'timestamp' => array(
'timestamp' => array(
'description' => __( 'Timestamp at which the URL metric was captured.', 'optimization-detective' ),
'type' => 'number',
'required' => true,
'readonly' => true, // Omit from REST API.
'minimum' => 0,
),
'elements' => array(
'elements' => array(
'description' => __( 'Element metrics', 'optimization-detective' ),
'type' => 'array',
'required' => true,
Expand Down Expand Up @@ -422,6 +446,15 @@ public function get_url(): string {
return $this->data['url'];
}

/**
* Gets queried object.
*
* @return QueriedObject|null Queried object.
*/
public function get_queried_object(): ?array {
return $this->data['queriedObject'] ?? null;
}

/**
* Gets viewport data.
*
Expand Down
7 changes: 7 additions & 0 deletions plugins/optimization-detective/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
* @typedef {import("web-vitals").LCPMetric} LCPMetric
* @typedef {import("./types.ts").ElementData} ElementData
* @typedef {import("./types.ts").URLMetric} URLMetric
* @typedef {import("./types.ts").QueriedObject} QueriedObject
* @typedef {import("./types.ts").URLMetricGroupStatus} URLMetricGroupStatus
* @typedef {import("./types.ts").Extension} Extension
* @typedef {import("./types.ts").ExtendedRootData} ExtendedRootData
Expand Down Expand Up @@ -239,6 +240,7 @@ function extendElementData( xpath, properties ) {
* @param {string} args.restApiEndpoint URL for where to send the detection data.
* @param {string} args.currentUrl Current URL.
* @param {string} args.urlMetricSlug Slug for URL Metric.
* @param {QueriedObject} [args.queriedObject] Queried object.
* @param {string} args.urlMetricHMAC HMAC for URL Metric storage.
* @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses.
* @param {number} args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock.
Expand All @@ -253,6 +255,7 @@ export default async function detect( {
restApiEndpoint,
currentUrl,
urlMetricSlug,
queriedObject,
urlMetricHMAC,
urlMetricGroupStatuses,
storageLockTTL,
Expand Down Expand Up @@ -446,6 +449,10 @@ export default async function detect( {
elements: [],
};

if ( queriedObject ) {
urlMetric.queriedObject = queriedObject;
}

const lcpMetric = lcpMetricCandidates.at( -1 );

for ( const elementIntersection of elementIntersections ) {
Expand Down
20 changes: 19 additions & 1 deletion plugins/optimization-detective/detection.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,20 @@ function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $
*/
$extension_module_urls = (array) apply_filters( 'od_extension_module_urls', array() );

// Obtain the queried object so when a URL Metric is stored the endpoint will know which object's cache to clean.
// Note that WP_Post_Type is intentionally excluded here since there is no equivalent to clean_post_cache(), clean_term_cache(), and clean_user_cache().
$queried_object = get_queried_object();
if ( $queried_object instanceof WP_Post ) {
$queried_object_type = 'post';
} elseif ( $queried_object instanceof WP_Term ) {
$queried_object_type = 'term';
} elseif ( $queried_object instanceof WP_User ) {
$queried_object_type = 'user';
} else {
$queried_object_type = null;
}
$queried_object_id = null === $queried_object_type ? null : (int) get_queried_object_id();

$current_url = od_get_current_url();
$detect_args = array(
'minViewportAspectRatio' => od_get_minimum_viewport_aspect_ratio(),
Expand All @@ -41,7 +55,11 @@ function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $
'restApiEndpoint' => rest_url( OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ),
'currentUrl' => $current_url,
'urlMetricSlug' => $slug,
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_url ),
'queriedObject' => null === $queried_object_type ? null : array(
'type' => $queried_object_type,
'id' => $queried_object_id,
),
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_url, $queried_object_type, $queried_object_id ),
'urlMetricGroupStatuses' => array_map(
static function ( OD_URL_Metric_Group $group ): array {
return array(
Expand Down
1 change: 1 addition & 0 deletions plugins/optimization-detective/hooks.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,4 @@
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_action( 'od_url_metric_stored', 'od_clean_queried_object_cache_for_stored_url_metric' );
25 changes: 14 additions & 11 deletions plugins/optimization-detective/storage/data.php
Original file line number Diff line number Diff line change
Expand Up @@ -151,14 +151,16 @@ function od_get_url_metrics_slug( array $query_vars ): string {
*
* @see od_verify_url_metrics_storage_hmac()
* @see od_get_url_metrics_slug()
* @todo This should also include an ETag as a parameter. See <https://github.com/WordPress/performance/issues/1466>.
*
* @param string $slug Slug (hash of normalized query vars).
* @param string $url URL.
*
* @param string $slug Slug (hash of normalized query vars).
* @param string $url URL.
* @param string|null $queried_object_type Queried object type.
* @param int|null $queried_object_id Queried object ID.
* @return string HMAC.
*/
function od_get_url_metrics_storage_hmac( string $slug, string $url ): string {
$action = "store_url_metric:$slug:$url";
function od_get_url_metrics_storage_hmac( string $slug, string $url, ?string $queried_object_type = null, ?int $queried_object_id = null ): string {
$action = "store_url_metric:$slug:$url:$queried_object_type:$queried_object_id";
return wp_hash( $action, 'nonce' );
}

Expand All @@ -172,14 +174,15 @@ function od_get_url_metrics_storage_hmac( string $slug, string $url ): string {
* @see od_get_url_metrics_storage_hmac()
* @see od_get_url_metrics_slug()
*
* @param string $hmac HMAC.
* @param string $slug Slug (hash of normalized query vars).
* @param String $url URL.
*
* @param string $hmac HMAC.
* @param string $slug Slug (hash of normalized query vars).
* @param String $url URL.
* @param string|null $queried_object_type Queried object type.
* @param int|null $queried_object_id Queried object ID.
* @return bool Whether the HMAC is valid.
*/
function od_verify_url_metrics_storage_hmac( string $hmac, string $slug, string $url ): bool {
return hash_equals( od_get_url_metrics_storage_hmac( $slug, $url ), $hmac );
function od_verify_url_metrics_storage_hmac( string $hmac, string $slug, string $url, ?string $queried_object_type = null, ?int $queried_object_id = null ): bool {
return hash_equals( od_get_url_metrics_storage_hmac( $slug, $url, $queried_object_type, $queried_object_id ), $hmac );
}

/**
Expand Down
41 changes: 40 additions & 1 deletion plugins/optimization-detective/storage/rest-api.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ function od_register_endpoint(): void {
'required' => true,
'pattern' => '^[0-9a-f]+$',
'validate_callback' => static function ( string $hmac, WP_REST_Request $request ) {
if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request->get_param( 'slug' ), $request->get_param( 'url' ) ) ) {
if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['url'], $request['queriedObject']['type'] ?? null, $request['queriedObject']['id'] ?? null ) ) {
return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) );
}
return true;
Expand Down Expand Up @@ -221,6 +221,7 @@ function od_handle_rest_request( WP_REST_Request $request ) {
* Fires whenever a URL Metric was successfully stored.
*
* @since 0.7.0
* @todo Add this to the README as documentation.
*
* @param OD_URL_Metric_Store_Request_Context $context Context about the successful URL Metric collection.
*/
Expand All @@ -241,3 +242,41 @@ function od_handle_rest_request( WP_REST_Request $request ) {
)
);
}

/**
* Cleans the cache for the queried object when it has a new URL Metric stored.
*
* This is intended to flush any page cache for the URL after the new URL Metric was submitted so that the optimizations
* which depend on that URL Metric can start to take effect. Furthermore, when a submitted URL Metric results in a full
* sample of URL Metric groups, then flushing the page cache will allow the next request to omit the detection script
* module altogether. When a page cache holds onto a cached page for a long time (e.g. a week), this will result in
* the stored URL Metrics being stale if they have the default freshness TTL of 1 day. Nevertheless, if no changes have
* been applied to a cached page then those stale URL Metrics should continue to result in an optimized page.
*
* This assumes that a page caching plugin flushes the page cache for a queried object via `clean_post_cache`,
* `clean_term_cache`, and `clean_user_cache` actions. Other actions may make sense to trigger as well as can be seen in
* {@link https://github.com/pantheon-systems/pantheon-advanced-page-cache/blob/e3b5552/README.md?plain=1#L314-L356}.
*
* @since n.e.x.t
*
* @param OD_URL_Metric_Store_Request_Context $context Context.
*/
function od_clean_queried_object_cache_for_stored_url_metric( OD_URL_Metric_Store_Request_Context $context ): void {
$queried_object = $context->url_metric->get_queried_object();
if ( ! is_array( $queried_object ) ) {
return;
}

// TODO: Should this instead call do_action() directly since we don't actually need to clear the object cache but just want to trigger page caches to flush the page caches?
switch ( $queried_object['type'] ) {
case 'post':
clean_post_cache( $queried_object['id'] );
break;
case 'term':
clean_term_cache( $queried_object['id'] );
break;
case 'user':
clean_user_cache( $queried_object['id'] );
break;
}
}
42 changes: 37 additions & 5 deletions plugins/optimization-detective/tests/storage/test-data.php
Original file line number Diff line number Diff line change
Expand Up @@ -286,18 +286,50 @@ public function test_od_get_url_metrics_slug(): void {
}
}

/**
* Data provider.
*
* @return array<string, mixed> Data.
*/
public function data_provider_to_test_hmac(): array {
return array(
'home' => array(
'url' => static function () {
return home_url();
},
'slug' => od_get_url_metrics_slug( array() ),
),
'post' => array(
'url' => static function () {
wp_update_post(
array(
'ID' => 1,
'post_type' => 'post',
'post_title' => 'Hello!',
)
);
return get_permalink( 1 );
},
'slug' => od_get_url_metrics_slug( array( 'p' => 1 ) ),
'queried_object_type' => 'post',
'queried_object_id' => 1,
),
);
}

/**
* Test od_get_url_metrics_storage_hmac() and od_verify_url_metrics_storage_hmac().
*
* @dataProvider data_provider_to_test_hmac
*
* @covers ::od_get_url_metrics_storage_hmac
* @covers ::od_verify_url_metrics_storage_hmac
*/
public function test_od_get_url_metrics_storage_hmac_and_od_verify_url_metrics_storage_hmac(): void {
$url = home_url( '/' );
$slug = od_get_url_metrics_slug( array() );
$hmac = od_get_url_metrics_storage_hmac( $slug, $url );
public function test_od_get_url_metrics_storage_hmac_and_od_verify_url_metrics_storage_hmac( Closure $get_url, string $slug, ?string $queried_object_type = null, ?int $queried_object_id = null ): void {
$url = $get_url();
$hmac = od_get_url_metrics_storage_hmac( $slug, $url, $queried_object_type, $queried_object_id );
$this->assertMatchesRegularExpression( '/^[0-9a-f]+$/', $hmac );
$this->assertTrue( od_verify_url_metrics_storage_hmac( $hmac, $slug, $url ) );
$this->assertTrue( od_verify_url_metrics_storage_hmac( $hmac, $slug, $url, $queried_object_type, $queried_object_id ) );
}

/**
Expand Down
Loading

0 comments on commit 619ac59

Please sign in to comment.