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 14 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 @@ -9,7 +9,8 @@
'intersectionRatio' => 1,
// Intentionally omitting resizedBoundingClientRect here to test behavior when data isn't supplied.
),
)
),
'embeds'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'intersectionRatio' => 1,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
)
),
'embeds'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
od_get_url_metrics_slug( od_get_normalized_query_vars() ),
$test_case->get_sample_url_metric(
array(
'eTag' => 'embeds',
'viewport_width' => $viewport_width,
'elements' => $elements,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'intersectionRatio' => 0,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
)
),
'embeds'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'intersectionRatio' => 1,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
)
),
'embeds'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
od_get_url_metrics_slug( od_get_normalized_query_vars() ),
$test_case->get_sample_url_metric(
array(
'eTag' => 'embeds',
'viewport_width' => $viewport_width,
'elements' => $elements,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
'intersectionRatio' => 0,
'resizedBoundingClientRect' => array_merge( $test_case->get_sample_dom_rect(), array( 'height' => 500 ) ),
),
)
),
'embeds'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::DIV]',
'isLCP' => true,
),
)
),
'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
$slug,
$test_case->get_sample_url_metric(
array(
'eTag' => 'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video',
'viewport_width' => $viewport_width,
'elements' => array(
array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]', // Note: This is intentionally not reflecting the IMG in the HTML below.
'isLCP' => true,
),
)
),
'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
'isLCP' => true,
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::IMG]',
),
)
),
'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
$slug,
$test_case->get_sample_url_metric(
array(
'eTag' => 'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video',
'viewport_width' => $viewport_width,
'elements' => array(
array(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
'xpath' => '/*[1][self::HTML]/*[2][self::BODY]/*[1][self::H1]',
'isLCP' => true,
),
)
),
'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video'
);
},
'buffer' => '
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ static function () use ( $mobile_breakpoint, $tablet_breakpoint ): array {
$slug,
$test_case->get_sample_url_metric(
array(
'eTag' => 'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video',
'viewport_width' => $viewport_width,
'element' => array(
'xpath' => sprintf( '/*[1][self::HTML]/*[2][self::BODY]/*[%d][self::DIV]', $div_index + 1 ),
Expand Down
2 changes: 1 addition & 1 deletion plugins/image-prioritizer/tests/test-helper.php
Original file line number Diff line number Diff line change
Expand Up @@ -205,7 +205,7 @@ public function data_provider_test_auto_sizes(): array {
* @phpstan-param array{ xpath: string, isLCP: bool, intersectionRatio: int } $element_metrics
*/
public function test_auto_sizes( array $element_metrics, string $buffer, string $expected ): void {
$this->populate_url_metrics( array( $element_metrics ) );
$this->populate_url_metrics( array( $element_metrics ), 'image-prioritizer/img,image-prioritizer/background-image,image-prioritizer/video' );

$html_start_doc = '<html lang="en"><head><meta charset="utf-8"><title>...</title></head><body>';
$html_end_doc = '</body></html>';
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 string
*/
private $current_etag;

/**
* Breakpoints in max widths.
*
Expand Down Expand Up @@ -94,11 +102,14 @@ 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 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 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: 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
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,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,7 +235,12 @@ public function is_complete(): bool {
}
$current_time = microtime( true );
foreach ( $this->url_metrics as $url_metric ) {
if ( $current_time > $url_metric->get_timestamp() + $this->freshness_ttl ) {
if (
$current_time > $url_metric->get_timestamp() + $this->freshness_ttl
||
// If the URL Metric's ETag doesn't match the current ETag, consider the URL metric as stale.
( null !== $this->collection && $url_metric->get_etag() !== $this->collection->get_current_etag() )
westonruter marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

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

If using hashes, then this would be better as:

Suggested change
( null !== $this->collection && $url_metric->get_etag() !== $this->collection->get_current_etag() )
( null !== $this->collection && ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) )

) {
return false;
}
}
Expand Down
22 changes: 21 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?: 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,12 @@ public static function get_json_schema(): array {
'required' => true,
'readonly' => true, // Omit from REST API.
),
'eTag' => array(
Copy link
Member

Choose a reason for hiding this comment

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

I wonder if we should make this just 'etag'. In HTTP it is "ETag", and a lower case "eTag" feels like it would maybe be something else, like an "electronic tag" or something.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Makes sense. Done in 08f8f8a.

'description' => __( 'The ETag for the URL Metric.', 'optimization-detective' ),
'type' => 'string',
westonruter marked this conversation as resolved.
Show resolved Hide resolved
'required' => false,
ShyamGadde marked this conversation as resolved.
Show resolved Hide resolved
'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 +317,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 +425,18 @@ public function get_uuid(): string {
return $this->data['uuid'];
}

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

/**
* 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 ETag for URL Metric.
* @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( 'eTag', currentETag );
Copy link
Member

Choose a reason for hiding this comment

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

It's a bit confusing that this is here rather than (also?) added as urlMetric.eTag (or urlMetric.etag). In the end they will need to be identical of course, so it does make sense. Just an observation. Maybe this should be called current_etag to better disambiguate?

Copy link
Contributor Author

@ShyamGadde ShyamGadde Nov 28, 2024

Choose a reason for hiding this comment

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

Renamed the arg in 371e998.

if ( typeof cachePurgePostId === 'number' ) {
url.searchParams.set(
'cache_purge_post_id',
Expand Down
4 changes: 3 additions & 1 deletion plugins/optimization-detective/detection.php
Original file line number Diff line number Diff line change
Expand Up @@ -85,16 +85,18 @@ 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();

$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' => $group_collection->get_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, $group_collection->get_current_etag(), $current_url, $cache_purge_post_id ),
'urlMetricGroupStatuses' => array_map(
static function ( OD_URL_Metric_Group $group ): array {
return array(
Expand Down
25 changes: 14 additions & 11 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,23 @@ function od_optimize_template_output_buffer( string $buffer ): string {
*/
do_action( 'od_register_tag_visitors', $tag_visitor_registry );

$visitors = iterator_to_array( $tag_visitor_registry );
$current_etag = implode( ',', array_keys( $visitors ) );
Copy link
Member

Choose a reason for hiding this comment

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

An important point here: similar to the slug, i think the etag actually should be hashed:

Suggested change
$current_etag = implode( ',', array_keys( $visitors ) );
$current_etag = md5( implode( ',', array_keys( $visitors ) ) );

The key reason here is that when expanding the scope here to address the parent issue #1466, there will be much more data to include in the ETag. So I suggest that there be a new function created, like od_compute_current_etag() which takes $tag_visitor_registry as an argument. For now it could look like:

function od_compute_current_etag( OD_Tag_Visitor_Registry $tag_visitor_registry ) {
    $data = array(
        'tag_visitors' => array_keys( iterator_to_array( $tag_visitor_registry ) ),
    );
    return md5( serialize( $data ) );
}

Then later when working on #1466 the $data can be expanded to include things like the IDs and post_modified times for the posts in the loop, what the current queried object is, and so on.

Copy link
Contributor Author

@ShyamGadde ShyamGadde Nov 28, 2024

Choose a reason for hiding this comment

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

Done in 8fe38f7.


$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 @@ -219,6 +219,7 @@ public static function store_url_metric( string $slug, OD_URL_Metric $new_url_me

$group_collection = new OD_URL_Metric_Group_Collection(
$url_metrics,
$new_url_metric->get_etag(),
od_get_breakpoint_max_widths(),
od_get_url_metrics_breakpoint_sample_size(),
od_get_url_metric_freshness_ttl()
Expand Down
Loading
Loading