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

Mark existing URL Metrics as stale when a new tag visitor is registered #1705

Merged
Show file tree
Hide file tree
Changes from 37 commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
f7295d8
Store the ETag along with the URL metric
ShyamGadde Nov 26, 2024
57b79e1
Factor in ETag of the URL metric for determining if it is stale
ShyamGadde Nov 26, 2024
7b48f26
Update test case to allow ETag to be optional in JSON Schema
ShyamGadde Nov 26, 2024
9999eea
Fix filter name typo in URL Metric schema extension
ShyamGadde Nov 26, 2024
3b01ea2
Factor in ETag when computing HMAC
ShyamGadde Nov 26, 2024
c801a65
Merge branch 'trunk' into add/mark-url-metrics-stale-on-tag-visitor-c…
ShyamGadde Nov 26, 2024
b84f62a
Make ETag a core property instead of adding it using a filter
ShyamGadde Nov 26, 2024
ae708cf
Return empty string instead of null when an existing URL metric doesn…
ShyamGadde Nov 27, 2024
e115134
Send ETag as REST API arg instead of URL metric property
ShyamGadde Nov 27, 2024
1cd62c0
Fix Test_OD_Storage_REST_API
ShyamGadde Nov 27, 2024
cb64e1d
Store the current ETag as a property of `OD_URL_Metric_Group_Collecti…
ShyamGadde Nov 27, 2024
a65fbad
Fix test_od_optimize_template_output_buffer
ShyamGadde Nov 27, 2024
5287f79
Fix test cases for embed-optimizer
ShyamGadde Nov 27, 2024
86023eb
Fix test-cases for image-prioritizer
ShyamGadde Nov 27, 2024
2eb6d98
Add comment to clarify future intent for ETag property
ShyamGadde Nov 28, 2024
2c7aee5
Update `get_etag` method to return `null` when ETag is absent
ShyamGadde Nov 28, 2024
821b48d
Handle PHPStan error for possible null ETag in store_url_metric() method
ShyamGadde Nov 28, 2024
52c486f
Improve variable naming
ShyamGadde Nov 28, 2024
63758ef
Add missing since tags
ShyamGadde Nov 28, 2024
08f8f8a
Improve naming for URL Metric schema properties
ShyamGadde Nov 28, 2024
371e998
Improve naming for REST API endpoint args
ShyamGadde Nov 28, 2024
ba2ea93
Update test cases to use the new property and arg names
ShyamGadde Nov 28, 2024
8fe38f7
Introduce new function to compute the current ETag
ShyamGadde Nov 28, 2024
e6078a9
Add pattern validation for ETag in schema
ShyamGadde Nov 28, 2024
309f9bd
Update test cases to use the new ETag format
ShyamGadde Nov 28, 2024
36cdeca
Merge branch 'trunk' of https://github.com/WordPress/performance into…
westonruter Nov 30, 2024
d1458d8
Refactor staleness check logic for URL Metrics
ShyamGadde Dec 1, 2024
c4eb613
Use non-empty-string as more precise type for ETag
westonruter Dec 1, 2024
695eecb
Refine ETag parameter in more places
westonruter Dec 1, 2024
5a36beb
Refactor static analysis accommodation code for when ETag is null
westonruter Dec 1, 2024
d855ae8
Fix test_add_url_metric
westonruter Dec 1, 2024
5e39581
Eliminate collection being an optional arg when constructing an OD_UR…
westonruter Dec 1, 2024
a430c56
Supply null as etag param to address static analysis complaint
westonruter Dec 1, 2024
bcc34c9
Temporarily work around errors related to non-hashes being supplied f…
westonruter Dec 1, 2024
4f0ac4c
Rename od_compute_current_etag() to od_get_current_etag() for parity …
westonruter Dec 1, 2024
ae3eab0
Use JSON-encoding instead of PHP serialization
westonruter Dec 1, 2024
a33eb28
Introduce od_current_url_metrics_etag_data filter for extensibility a…
westonruter Dec 1, 2024
d37d671
Use md5() directly when current ETag is not needed
westonruter Dec 1, 2024
92dc354
Remove extra empty line
ShyamGadde Dec 1, 2024
85540e6
Add validation for current_etag paramter in OD_URL_Metric_Group_Colle…
ShyamGadde Dec 1, 2024
731fe91
Use passed URL in test helper instead of default home_url()
ShyamGadde Dec 1, 2024
c3b7344
Update test case for group collection constructor
ShyamGadde Dec 1, 2024
9fcfb7f
Add tests for OD_URL_Metric_Group::is_complete
ShyamGadde Dec 1, 2024
b225fba
Add tests for OD_URL_Metric::get_etag
ShyamGadde Dec 1, 2024
7b01e50
Add missing `@covers ::get_url` annotation in test case
ShyamGadde Dec 1, 2024
4456563
Harden ETag regex to disregard newlines
westonruter Dec 1, 2024
034fe46
Add test for od_get_current_url_metrics_etag()
westonruter Dec 2, 2024
02e9759
Add docs for od_current_url_metrics_etag_data filter
westonruter Dec 2, 2024
54183af
Eliminate constructing etag via OD_Tag_Visitor_Registry() unless side…
westonruter Dec 2, 2024
6960a28
Fix checked logic in old_url_metric test case
westonruter Dec 2, 2024
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
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ public function set_up(): void {
if ( ! defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
$this->markTestSkipped( 'Optimization Detective is not active.' );
}

// Normalize the data for computing the current URL Metrics ETag to work around the issue where there is no
// global variable storing the OD_Tag_Visitor_Registry instance along with any registered tag visitors, so
// during set up we do not know what the ETag will look like. The current ETag is only established when
// the output begins to be processed by od_optimize_template_output_buffer().
add_filter( 'od_current_url_metrics_etag_data', '__return_empty_array' );
}

/**
Expand Down
13 changes: 13 additions & 0 deletions plugins/image-prioritizer/tests/test-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,19 @@
class Test_Image_Prioritizer_Helper extends WP_UnitTestCase {
use Optimization_Detective_Test_Helpers;

/**
* Runs the routine before each test is executed.
*/
public function set_up(): void {
parent::set_up();

// Normalize the data for computing the current URL Metrics ETag to work around the issue where there is no
// global variable storing the OD_Tag_Visitor_Registry instance along with any registered tag visitors, so
// during set up we do not know what the ETag will look like. The current ETag is only established when
// the output begins to be processed by od_optimize_template_output_buffer().
add_filter( 'od_current_url_metrics_etag_data', '__return_empty_array' );
}

/**
* @return array<string, array<string, mixed>>
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,14 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega
*/
private $groups;

/**
* The current ETag.
*
* @since n.e.x.t
* @var non-empty-string
*/
private $current_etag;

/**
* Breakpoints in max widths.
*
Expand Down Expand Up @@ -93,12 +101,15 @@ final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggrega
*
* @throws InvalidArgumentException When an invalid argument is supplied.
*
* @param OD_URL_Metric[] $url_metrics URL Metrics.
* @param int[] $breakpoints Breakpoints in max widths.
* @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints.
* @param int $freshness_ttl Freshness age (TTL) for a given URL Metric.
* @param OD_URL_Metric[] $url_metrics URL Metrics.
* @param non-empty-string $current_etag The current ETag.
* @param int[] $breakpoints Breakpoints in max widths.
* @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints.
* @param int $freshness_ttl Freshness age (TTL) for a given URL Metric.
*/
public function __construct( array $url_metrics, array $breakpoints, int $sample_size, int $freshness_ttl ) {
public function __construct( array $url_metrics, string $current_etag, array $breakpoints, int $sample_size, int $freshness_ttl ) {
$this->current_etag = $current_etag;

// Set breakpoints.
sort( $breakpoints );
$breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) );
Expand Down Expand Up @@ -160,6 +171,17 @@ public function __construct( array $url_metrics, array $breakpoints, int $sample
}
}

/**
* Gets the current ETag.
*
* @since n.e.x.t
*
* @return non-empty-string Current ETag.
*/
public function get_current_etag(): string {
return $this->current_etag;
}

/**
* Gets the first URL Metric group.
*
Expand Down Expand Up @@ -613,6 +635,7 @@ public function count(): int {
* @since 0.3.1
*
* @return array{
* current_etag: non-empty-string,
* breakpoints: positive-int[],
* freshness_ttl: 0|positive-int,
* sample_size: positive-int,
Expand All @@ -631,6 +654,7 @@ public function count(): int {
*/
public function jsonSerialize(): array {
return array(
'current_etag' => $this->current_etag,
'breakpoints' => $this->breakpoints,
'freshness_ttl' => $this->freshness_ttl,
'sample_size' => $this->sample_size,
Expand Down
44 changes: 27 additions & 17 deletions plugins/optimization-detective/class-od-url-metric-group.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSer
/**
* Collection that this instance belongs to.
*
* @var OD_URL_Metric_Group_Collection|null
* @var OD_URL_Metric_Group_Collection
*/
private $collection;

Expand All @@ -82,16 +82,19 @@ final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSer
/**
* Constructor.
*
* This class should never be directly constructed. It should only be constructed by the {@see OD_URL_Metric_Group_Collection::create_groups()}.
*
* @access private
* @throws InvalidArgumentException If arguments are invalid.
*
* @param OD_URL_Metric[] $url_metrics URL Metrics to add to the group.
* @param int $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.
* @param int $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.
* @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints.
* @param int $freshness_ttl Freshness age (TTL) for a given URL Metric.
* @param OD_URL_Metric_Group_Collection|null $collection Collection that this instance belongs to. Optional.
* @param OD_URL_Metric[] $url_metrics URL Metrics to add to the group.
* @param int $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.
* @param int $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.
* @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints.
* @param int $freshness_ttl Freshness age (TTL) for a given URL Metric.
* @param OD_URL_Metric_Group_Collection $collection Collection that this instance belongs to. Optional.
*/
public function __construct( array $url_metrics, int $minimum_viewport_width, int $maximum_viewport_width, int $sample_size, int $freshness_ttl, ?OD_URL_Metric_Group_Collection $collection = null ) {
public function __construct( array $url_metrics, int $minimum_viewport_width, int $maximum_viewport_width, int $sample_size, int $freshness_ttl, OD_URL_Metric_Group_Collection $collection ) {
if ( $minimum_viewport_width < 0 ) {
throw new InvalidArgumentException(
esc_html__( 'The minimum viewport width must be at least zero.', 'optimization-detective' )
Expand Down Expand Up @@ -135,12 +138,8 @@ public function __construct( array $url_metrics, int $minimum_viewport_width, in
);
}
$this->freshness_ttl = $freshness_ttl;

if ( ! is_null( $collection ) ) {
$this->collection = $collection;
}

$this->url_metrics = $url_metrics;
$this->collection = $collection;
$this->url_metrics = $url_metrics;
}

/**
Expand Down Expand Up @@ -191,9 +190,7 @@ public function add_url_metric( OD_URL_Metric $url_metric ): void {
}

$this->result_cache = array();
if ( ! is_null( $this->collection ) ) {
$this->collection->clear_cache();
}
$this->collection->clear_cache();

$url_metric->set_group( $this );
$this->url_metrics[] = $url_metric;
Expand All @@ -220,6 +217,8 @@ static function ( OD_URL_Metric $a, OD_URL_Metric $b ): int {
* A group is complete if it has the full sample size of URL Metrics
* and all of these URL Metrics are fresh.
*
* @since n.e.x.t If the current environment's generated ETag does not match the URL Metric's ETag, the URL Metric is considered stale.
*
* @return bool Whether complete.
*/
public function is_complete(): bool {
Expand All @@ -233,9 +232,20 @@ public function is_complete(): bool {
}
$current_time = microtime( true );
foreach ( $this->url_metrics as $url_metric ) {
// The URL Metric is too old to be fresh.
if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) {
return false;
}

// The ETag is not populated yet, so this is stale. Eventually this will be required.
if ( $url_metric->get_etag() === null ) {
return false;
}

// The ETag of the URL Metric does not match the current ETag for the collection, so it is stale.
if ( ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) ) {
return false;
}
westonruter marked this conversation as resolved.
Show resolved Hide resolved
}

return true;
Expand Down
25 changes: 24 additions & 1 deletion plugins/optimization-detective/class-od-url-metric.php
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
* }
* @phpstan-type Data array{
* uuid: non-empty-string,
* etag?: non-empty-string,
* url: non-empty-string,
* timestamp: float,
* viewport: ViewportRect,
Expand Down Expand Up @@ -155,6 +156,7 @@ public function set_group( OD_URL_Metric_Group $group ): void {
* Gets JSON schema for URL Metric.
*
* @since 0.1.0
* @since n.e.x.t Added the 'etag' property to the schema.
*
* @todo Cache the return value?
*
Expand Down Expand Up @@ -208,6 +210,15 @@ public static function get_json_schema(): array {
'required' => true,
'readonly' => true, // Omit from REST API.
),
'etag' => array(
'description' => __( 'The ETag for the URL Metric.', 'optimization-detective' ),
'type' => 'string',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'pattern' => '^[0-9a-f]{32}$',
'minLength' => 32,
'maxLength' => 32,
'required' => false, // To be made required in a future release.
'readonly' => true, // Omit from REST API.
),
'url' => array(
'description' => __( 'The URL for which the metric was obtained.', 'optimization-detective' ),
'type' => 'string',
Expand Down Expand Up @@ -309,7 +320,7 @@ public static function get_json_schema(): array {
$schema['properties']['elements']['items']['properties'] = self::extend_schema_with_optional_properties(
$schema['properties']['elements']['items']['properties'],
$additional_properties,
'od_url_metric_schema_root_additional_properties'
'od_url_metric_schema_element_item_additional_properties'
westonruter marked this conversation as resolved.
Show resolved Hide resolved
);
}

Expand Down Expand Up @@ -417,6 +428,18 @@ public function get_uuid(): string {
return $this->data['uuid'];
}

/**
* Gets ETag.
*
* @since n.e.x.t
*
* @return non-empty-string|null ETag.
*/
public function get_etag(): ?string {
westonruter marked this conversation as resolved.
Show resolved Hide resolved
// Since the ETag is optional for now, return null for old URL Metrics that do not have one.
return $this->data['etag'] ?? null;
}

/**
* Gets URL.
*
Expand Down
3 changes: 3 additions & 0 deletions plugins/optimization-detective/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -237,6 +237,7 @@ function extendElementData( xpath, properties ) {
* @param {number} args.maxViewportAspectRatio Maximum aspect ratio allowed for the viewport.
* @param {boolean} args.isDebug Whether to show debug messages.
* @param {string} args.restApiEndpoint URL for where to send the detection data.
* @param {string} args.currentETag Current ETag.
* @param {string} args.currentUrl Current URL.
* @param {string} args.urlMetricSlug Slug for URL Metric.
* @param {number|null} args.cachePurgePostId Cache purge post ID.
Expand All @@ -252,6 +253,7 @@ export default async function detect( {
isDebug,
extensionModuleUrls,
restApiEndpoint,
currentETag,
currentUrl,
urlMetricSlug,
cachePurgePostId,
Expand Down Expand Up @@ -539,6 +541,7 @@ export default async function detect( {

const url = new URL( restApiEndpoint );
url.searchParams.set( 'slug', urlMetricSlug );
url.searchParams.set( 'current_etag', currentETag );
if ( typeof cachePurgePostId === 'number' ) {
url.searchParams.set(
'cache_purge_post_id',
Expand Down
6 changes: 5 additions & 1 deletion plugins/optimization-detective/detection.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,20 @@ function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $
$cache_purge_post_id = od_get_cache_purge_post_id();

$current_url = od_get_current_url();

$current_etag = $group_collection->get_current_etag();

$detect_args = array(
'minViewportAspectRatio' => od_get_minimum_viewport_aspect_ratio(),
'maxViewportAspectRatio' => od_get_maximum_viewport_aspect_ratio(),
'isDebug' => WP_DEBUG,
'extensionModuleUrls' => $extension_module_urls,
'restApiEndpoint' => rest_url( OD_REST_API_NAMESPACE . OD_URL_METRICS_ROUTE ),
'currentETag' => $current_etag,
'currentUrl' => $current_url,
'urlMetricSlug' => $slug,
'cachePurgePostId' => od_get_cache_purge_post_id(),
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_url, $cache_purge_post_id ),
'urlMetricHMAC' => od_get_url_metrics_storage_hmac( $slug, $current_etag, $current_url, $cache_purge_post_id ),
'urlMetricGroupStatuses' => array_map(
static function ( OD_URL_Metric_Group $group ): array {
return array(
Expand Down
22 changes: 12 additions & 10 deletions plugins/optimization-detective/optimization.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,16 +195,6 @@ function od_optimize_template_output_buffer( string $buffer ): string {
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
$post = OD_URL_Metrics_Post_Type::get_post( $slug );

$group_collection = new OD_URL_Metric_Group_Collection(
$post instanceof WP_Post ? OD_URL_Metrics_Post_Type::get_url_metrics_from_post( $post ) : array(),
od_get_breakpoint_max_widths(),
od_get_url_metrics_breakpoint_sample_size(),
od_get_url_metric_freshness_ttl()
);

// 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();

$tag_visitor_registry = new OD_Tag_Visitor_Registry();

/**
Expand All @@ -216,10 +206,22 @@ function od_optimize_template_output_buffer( string $buffer ): string {
*/
do_action( 'od_register_tag_visitors', $tag_visitor_registry );

$current_etag = od_get_current_url_metrics_etag( $tag_visitor_registry );
$group_collection = new OD_URL_Metric_Group_Collection(
$post instanceof WP_Post ? OD_URL_Metrics_Post_Type::get_url_metrics_from_post( $post ) : array(),
$current_etag,
od_get_breakpoint_max_widths(),
od_get_url_metrics_breakpoint_sample_size(),
od_get_url_metric_freshness_ttl()
);
$link_collection = new OD_Link_Collection();
$tag_visitor_context = new OD_Tag_Visitor_Context( $processor, $group_collection, $link_collection );
$current_tag_bookmark = 'optimization_detective_current_tag';
$visitors = iterator_to_array( $tag_visitor_registry );

// 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();

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
Expand Up @@ -217,8 +217,18 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me
$url_metrics = array();
}

$etag = $new_url_metric->get_etag();
if ( null === $etag ) {
// This case actually will never occur in practice because the store_url_metric function is only called
// in the REST API endpoint where the ETag parameter is required. It is here exclusively for the sake of
// PHPStan's static analysis. This entire condition can be removed in a future release when the 'etag'
// property becomes required.
return new WP_Error( 'missing_etag' );
}

$group_collection = new OD_URL_Metric_Group_Collection(
$url_metrics,
$etag,
od_get_breakpoint_max_widths(),
od_get_url_metrics_breakpoint_sample_size(),
od_get_url_metric_freshness_ttl()
Expand Down
Loading
Loading