Changeset 3240307
- Timestamp:
- 02/13/2025 07:42:05 PM (9 months ago)
- Location:
- optimization-detective
- Files:
-
- 54 edited
- 1 copied
-
tags/1.0.0-beta2 (copied) (copied from optimization-detective/trunk)
-
tags/1.0.0-beta2/class-od-data-validation-exception.php (modified) (1 diff)
-
tags/1.0.0-beta2/class-od-element.php (modified) (1 diff)
-
tags/1.0.0-beta2/class-od-html-tag-processor.php (modified) (15 diffs)
-
tags/1.0.0-beta2/class-od-link-collection.php (modified) (11 diffs)
-
tags/1.0.0-beta2/class-od-tag-visitor-context.php (modified) (6 diffs)
-
tags/1.0.0-beta2/class-od-tag-visitor-registry.php (modified) (8 diffs)
-
tags/1.0.0-beta2/class-od-url-metric-group-collection.php (modified) (18 diffs)
-
tags/1.0.0-beta2/class-od-url-metric-group.php (modified) (16 diffs)
-
tags/1.0.0-beta2/class-od-url-metric.php (modified) (15 diffs)
-
tags/1.0.0-beta2/class-od-visited-tag-state.php (modified) (1 diff)
-
tags/1.0.0-beta2/detect.js (modified) (9 diffs)
-
tags/1.0.0-beta2/detect.min.js (modified) (1 diff)
-
tags/1.0.0-beta2/detection.php (modified) (5 diffs)
-
tags/1.0.0-beta2/docs/extensions.md (modified) (2 diffs)
-
tags/1.0.0-beta2/docs/hooks.md (modified) (4 diffs)
-
tags/1.0.0-beta2/helper.php (modified) (2 diffs)
-
tags/1.0.0-beta2/hooks.php (modified) (1 diff)
-
tags/1.0.0-beta2/load.php (modified) (2 diffs)
-
tags/1.0.0-beta2/optimization.php (modified) (3 diffs)
-
tags/1.0.0-beta2/readme.txt (modified) (2 diffs)
-
tags/1.0.0-beta2/site-health.php (modified) (5 diffs)
-
tags/1.0.0-beta2/storage/class-od-storage-lock.php (modified) (5 diffs)
-
tags/1.0.0-beta2/storage/class-od-url-metric-store-request-context.php (modified) (2 diffs)
-
tags/1.0.0-beta2/storage/class-od-url-metrics-post-type.php (modified) (7 diffs)
-
tags/1.0.0-beta2/storage/data.php (modified) (13 diffs)
-
tags/1.0.0-beta2/storage/rest-api.php (modified) (4 diffs)
-
tags/1.0.0-beta2/types.ts (modified) (1 diff)
-
trunk/class-od-data-validation-exception.php (modified) (1 diff)
-
trunk/class-od-element.php (modified) (1 diff)
-
trunk/class-od-html-tag-processor.php (modified) (15 diffs)
-
trunk/class-od-link-collection.php (modified) (11 diffs)
-
trunk/class-od-tag-visitor-context.php (modified) (6 diffs)
-
trunk/class-od-tag-visitor-registry.php (modified) (8 diffs)
-
trunk/class-od-url-metric-group-collection.php (modified) (18 diffs)
-
trunk/class-od-url-metric-group.php (modified) (16 diffs)
-
trunk/class-od-url-metric.php (modified) (15 diffs)
-
trunk/class-od-visited-tag-state.php (modified) (1 diff)
-
trunk/detect.js (modified) (9 diffs)
-
trunk/detect.min.js (modified) (1 diff)
-
trunk/detection.php (modified) (5 diffs)
-
trunk/docs/extensions.md (modified) (2 diffs)
-
trunk/docs/hooks.md (modified) (4 diffs)
-
trunk/helper.php (modified) (2 diffs)
-
trunk/hooks.php (modified) (1 diff)
-
trunk/load.php (modified) (2 diffs)
-
trunk/optimization.php (modified) (3 diffs)
-
trunk/readme.txt (modified) (2 diffs)
-
trunk/site-health.php (modified) (5 diffs)
-
trunk/storage/class-od-storage-lock.php (modified) (5 diffs)
-
trunk/storage/class-od-url-metric-store-request-context.php (modified) (2 diffs)
-
trunk/storage/class-od-url-metrics-post-type.php (modified) (7 diffs)
-
trunk/storage/data.php (modified) (13 diffs)
-
trunk/storage/rest-api.php (modified) (4 diffs)
-
trunk/types.ts (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
optimization-detective/tags/1.0.0-beta2/class-od-data-validation-exception.php
r3229869 r3240307 17 17 * 18 18 * @since 0.1.0 19 * @access private20 19 */ 21 20 final class OD_Data_Validation_Exception extends Exception {} -
optimization-detective/tags/1.0.0-beta2/class-od-element.php
r3229869 r3240307 22 22 * 23 23 * @since 0.7.0 24 * @access private25 24 */ 26 25 class OD_Element implements ArrayAccess, JsonSerializable { -
optimization-detective/tags/1.0.0-beta2/class-od-html-tag-processor.php
r3229869 r3240307 17 17 * 18 18 * @since 0.1.1 19 * @access private20 19 */ 21 20 final class OD_HTML_Tag_Processor extends WP_HTML_Tag_Processor { … … 150 149 * 151 150 * @since 0.4.0 152 * @var string[]151 * @var non-empty-string[] 153 152 */ 154 153 private $open_stack_tags = array(); … … 161 160 * 162 161 * @since 1.0.0 163 * @var array<array< string, string>>162 * @var array<array<non-empty-string, string>> 164 163 */ 165 164 private $open_stack_attributes = array(); … … 169 168 * 170 169 * @since 0.4.0 171 * @var int[]170 * @var non-negative-int[] 172 171 */ 173 172 private $open_stack_indices = array(); … … 182 181 * 183 182 * @since 0.4.0 184 * @var array<string, array{tags: string[], attributes: array<array<string, string>>, indices:int[]}>183 * @var array<string, array{tags: non-empty-string[], attributes: array<array<non-empty-string, string>>, indices: non-negative-int[]}> 185 184 */ 186 185 private $bookmarked_open_stacks = array(); … … 222 221 * 223 222 * @since 0.4.0 224 * @var array< string, string[]>223 * @var array<non-empty-string, string[]> 225 224 */ 226 225 private $buffered_text_replacements = array(); … … 239 238 * 240 239 * @since 0.6.0 241 * @var int240 * @var non-negative-int 242 241 * @see self::next_token() 243 242 * @see self::seek() … … 293 292 * @since 0.4.0 294 293 * 295 * @param string|null $tag_name Tag name, if not provided then the current tag is used. Optional.294 * @param non-empty-string|null $tag_name Tag name, if not provided then the current tag is used. Optional. 296 295 * @return bool Whether to expect a closer for the tag. 297 296 */ … … 337 336 return true; 338 337 } 338 /** 339 * Tag name. 340 * 341 * @var non-empty-string $tag_name 342 */ 339 343 340 344 if ( $this->previous_tag_without_closer ) { … … 423 427 * @see self::seek() 424 428 * 425 * @return int Count of times the cursor has moved.429 * @return non-negative-int Count of times the cursor has moved. 426 430 */ 427 431 public function get_cursor_move_count(): int { … … 459 463 * @since 0.4.0 460 464 * 461 * @param string$name Meta attribute name.462 * @param string|true $value Value.465 * @param non-empty-string $name Meta attribute name. 466 * @param string|true $value Value. 463 467 * @return bool Whether an attribute was set. 464 468 */ … … 490 494 * @see WP_HTML_Processor::get_current_depth() 491 495 * 492 * @return int Nesting-depth of current location in the document.496 * @return non-negative-int Nesting-depth of current location in the document. 493 497 */ 494 498 public function get_current_depth(): int { … … 566 570 * @since 0.9.0 Renamed from get_breadcrumbs() to get_indexed_breadcrumbs(). 567 571 * 568 * @return Generator<array{ string, int, array<string, string>}> Breadcrumb.572 * @return Generator<array{non-empty-string, non-negative-int, array<non-empty-string, string>}> Breadcrumb. 569 573 */ 570 574 private function get_indexed_breadcrumbs(): Generator { … … 585 589 * @since 1.0.0 586 590 * 587 * @return array< string, string> Disambiguating attributes.591 * @return array<non-empty-string, string> Disambiguating attributes. 588 592 */ 589 593 private function get_disambiguating_attributes(): array { … … 618 622 * @see WP_HTML_Processor::get_breadcrumbs() 619 623 * 620 * @return string[] Array of tag names representing path to matched node.624 * @return non-empty-string[] Array of tag names representing path to matched node. 621 625 */ 622 626 public function get_breadcrumbs(): array { -
optimization-detective/tags/1.0.0-beta2/class-od-link-collection.php
r3229869 r3240307 19 19 * attributes: LinkAttributes, 20 20 * minimum_viewport_width: int<0, max>|null, 21 * maximum_viewport_width: positive-int|null21 * maximum_viewport_width: int<1, max>|null 22 22 * } 23 23 * … … 38 38 * @since 0.3.0 39 39 * @since 0.4.0 Renamed from OD_Preload_Link_Collection. 40 * @access private41 40 */ 42 41 final class OD_Link_Collection implements Countable { … … 45 44 * Links grouped by rel type. 46 45 * 46 * @since 0.4.0 47 * 47 48 * @var array<string, Link[]> 48 49 */ … … 52 53 * Adds link. 53 54 * 55 * @since 0.3.0 56 * 54 57 * @phpstan-param LinkAttributes $attributes 55 58 * 56 * @param array $attributes Attributes.57 * @param int<0, max>|null $minimum_viewport_width Minimum widthor null if not bounded or relevant.58 * @param positive-int|null $maximum_viewport_width Maximum widthor null if not bounded (i.e. infinity) or relevant.59 * @param array $attributes Attributes. 60 * @param int<0, max>|null $minimum_viewport_width Minimum width (exclusive) or null if not bounded or relevant. 61 * @param int<1, max>|null $maximum_viewport_width Maximum width (inclusive) or null if not bounded (i.e. infinity) or relevant. 59 62 * 60 63 * @throws InvalidArgumentException When invalid arguments are provided. … … 112 115 * together. Also, add media attributes to the links. 113 116 * 117 * @since 0.4.0 118 * 114 119 * @return LinkAttributes[] Prepared links with adjacent-duplicates merged together and media attributes added. 115 120 */ … … 133 138 /** 134 139 * Merges consecutive links. 140 * 141 * @since 0.4.0 135 142 * 136 143 * @param Link[] $links Links. … … 195 202 is_int( $last_link['maximum_viewport_width'] ) 196 203 && 197 $last_link['maximum_viewport_width'] + 1=== $link['minimum_viewport_width']204 $last_link['maximum_viewport_width'] === $link['minimum_viewport_width'] 198 205 ) { 199 $last_link['maximum_viewport_width'] = max( $last_link['maximum_viewport_width'], $link['maximum_viewport_width'] );206 $last_link['maximum_viewport_width'] = null === $link['maximum_viewport_width'] ? null : max( $last_link['maximum_viewport_width'], $link['maximum_viewport_width'] ); 200 207 201 208 // Update the last link with the new maximum viewport width. … … 229 236 * Gets the HTML for the link tags. 230 237 * 238 * @since 0.3.0 239 * 231 240 * @return string Link tags HTML. 232 241 */ … … 250 259 * Constructs the Link HTTP response header. 251 260 * 252 * @return string|null Link HTTP response header, or null if there are none. 261 * @since 0.4.0 262 * 263 * @return non-empty-string|null Link HTTP response header, or null if there are none. 253 264 */ 254 265 public function get_response_header(): ?string { … … 256 267 257 268 foreach ( $this->get_prepared_links() as $link ) { 258 // The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded. 259 $link['href'] = isset( $link['href'] ) ? esc_url_raw( $link['href'] ) : 'about:blank'; 260 $link_header = '<' . $link['href'] . '>'; 269 if ( isset( $link['href'] ) ) { 270 $link['href'] = $this->encode_url_for_response_header( $link['href'] ); 271 } else { 272 // The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded. 273 $link['href'] = 'about:blank'; 274 } 275 276 // Encode the URLs in the srcset. 277 if ( isset( $link['imagesrcset'] ) ) { 278 $link['imagesrcset'] = join( 279 ', ', 280 array_map( 281 function ( $image_candidate ) { 282 // Parse out the URL to separate it from the descriptor. 283 $image_candidate_parts = (array) preg_split( '/\s+/', (string) $image_candidate, 2 ); 284 285 // Encode the URL. 286 $image_candidate_parts[0] = $this->encode_url_for_response_header( (string) $image_candidate_parts[0] ); 287 288 // Re-join the URL with the descriptor. 289 return implode( ' ', $image_candidate_parts ); 290 }, 291 (array) preg_split( '/\s*,\s*/', $link['imagesrcset'] ) 292 ) 293 ); 294 } 295 296 $link_header = '<' . $link['href'] . '>'; 261 297 unset( $link['href'] ); 262 298 foreach ( $link as $name => $value ) { … … 286 322 287 323 /** 324 * Encodes a URL for serving in an HTTP response header. 325 * 326 * @since n.e.x.t 327 * 328 * @param string $url URL to percent encode. Any existing percent encodings will first be decoded. 329 * @return string Percent-encoded URL. 330 */ 331 private function encode_url_for_response_header( string $url ): string { 332 $decoded_url = urldecode( $url ); 333 334 // Encode characters not allowed in a URL per RFC 3986 (anything that is not among the reserved and unreserved characters). 335 $encoded_url = (string) preg_replace_callback( 336 '/[^A-Za-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]/', 337 static function ( $matches ) { 338 return rawurlencode( $matches[0] ); 339 }, 340 $decoded_url 341 ); 342 return esc_url_raw( $encoded_url ); 343 } 344 345 /** 288 346 * Counts the links. 347 * 348 * @since 0.3.0 289 349 * 290 350 * @return non-negative-int Link count. -
optimization-detective/tags/1.0.0-beta2/class-od-tag-visitor-context.php
r3229869 r3240307 18 18 * @since 0.4.0 19 19 * 20 * @property-read OD_URL_Metric_Group_Collection $url_metrics_group_collection Deprecated property accessed via magic getter. Use the url_metric_group_collection property instead. 20 * @property-read OD_HTML_Tag_Processor $processor HTML tag processor. 21 * @property-read OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 22 * @property-read OD_Link_Collection $link_collection Link collection. 23 * @property-read positive-int|null $url_metrics_id ID for the od_url_metrics post which provided the URL Metrics in the collection. 24 * @property-read OD_URL_Metric_Group_Collection $url_metrics_group_collection Deprecated alias for the $url_metric_group_collection property. 21 25 */ 22 26 final class OD_Tag_Visitor_Context { … … 27 31 * @since 0.4.0 28 32 * @var OD_HTML_Tag_Processor 29 * @readonly30 33 */ 31 p ublic$processor;34 private $processor; 32 35 33 36 /** … … 36 39 * @since 0.4.0 37 40 * @var OD_URL_Metric_Group_Collection 38 * @readonly39 41 */ 40 p ublic$url_metric_group_collection;42 private $url_metric_group_collection; 41 43 42 44 /** … … 45 47 * @since 0.4.0 46 48 * @var OD_Link_Collection 47 * @readonly48 49 */ 49 public $link_collection; 50 private $link_collection; 51 52 /** 53 * ID for the od_url_metrics post which provided the URL Metrics in the collection. 54 * 55 * May be null if no post has been created yet. 56 * 57 * @since 1.0.0 58 * @var positive-int|null 59 */ 60 private $url_metrics_id; 50 61 51 62 /** 52 63 * Visited tag state. 64 * 65 * Important: This object is not exposed directly by the getter. It is only exposed via {@see self::track_tag()}. 53 66 * 54 67 * @since 1.0.0 … … 66 79 * @param OD_Link_Collection $link_collection Link collection. 67 80 * @param OD_Visited_Tag_State $visited_tag_state Visited tag state. 81 * @param positive-int|null $url_metrics_id ID for the od_url_metrics post which provided the URL Metrics in the collection. May be null if no post has been created yet. 68 82 */ 69 public function __construct( OD_HTML_Tag_Processor $processor, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_Link_Collection $link_collection, OD_Visited_Tag_State $visited_tag_state ) {83 public function __construct( OD_HTML_Tag_Processor $processor, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_Link_Collection $link_collection, OD_Visited_Tag_State $visited_tag_state, ?int $url_metrics_id ) { 70 84 $this->processor = $processor; 71 85 $this->url_metric_group_collection = $url_metric_group_collection; 72 86 $this->link_collection = $link_collection; 73 87 $this->visited_tag_state = $visited_tag_state; 88 $this->url_metrics_id = $url_metrics_id; 74 89 } 75 90 … … 86 101 87 102 /** 88 * Gets deprecatedproperty.103 * Gets a property. 89 104 * 90 105 * @since 0.7.0 91 * @todo Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore.92 106 * 93 107 * @param string $name Property name. 94 * @return OD_URL_Metric_Group_Collection URL Metric group collection.108 * @return mixed Property value. 95 109 * 96 110 * @throws Error When property is unknown. 97 111 */ 98 public function __get( string $name ): OD_URL_Metric_Group_Collection { 99 if ( 'url_metrics_group_collection' === $name ) { 100 _doing_it_wrong( 101 __CLASS__ . '::$url_metrics_group_collection', 102 esc_html( 103 sprintf( 104 /* translators: %s is class member variable name */ 105 __( 'Use %s instead.', 'optimization-detective' ), 106 __CLASS__ . '::$url_metric_group_collection' 112 public function __get( string $name ) { 113 // Note that there is intentionally not a case for 'visited_tag_state'. 114 switch ( $name ) { 115 case 'processor': 116 return $this->processor; 117 case 'link_collection': 118 return $this->link_collection; 119 case 'url_metrics_id': 120 return $this->url_metrics_id; 121 case 'url_metric_group_collection': 122 return $this->url_metric_group_collection; 123 case 'url_metrics_group_collection': 124 // TODO: Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore. 125 _doing_it_wrong( 126 esc_html( __CLASS__ . '::$' . $name ), 127 esc_html( 128 sprintf( 129 /* translators: %s is class member variable name */ 130 __( 'Use %s instead.', 'optimization-detective' ), 131 __CLASS__ . '::$url_metric_group_collection' 132 ) 133 ), 134 'optimization-detective 0.7.0' 135 ); 136 return $this->url_metric_group_collection; 137 default: 138 throw new Error( 139 esc_html( 140 sprintf( 141 /* translators: %s is class member variable name */ 142 __( 'Unknown property %s.', 'optimization-detective' ), 143 __CLASS__ . '::$' . $name 144 ) 107 145 ) 108 ), 109 'optimization-detective 0.7.0' 110 ); 111 return $this->url_metric_group_collection; 146 ); 112 147 } 113 throw new Error(114 esc_html(115 sprintf(116 /* translators: %s is class member variable name */117 __( 'Unknown property %s.', 'optimization-detective' ),118 __CLASS__ . '::$' . $name119 )120 )121 );122 148 } 123 149 } -
optimization-detective/tags/1.0.0-beta2/class-od-tag-visitor-registry.php
r3229869 r3240307 21 21 * 22 22 * @since 0.3.0 23 * @access private24 23 */ 25 24 final class OD_Tag_Visitor_Registry implements Countable, IteratorAggregate { … … 28 27 * Visitors. 29 28 * 30 * @var array<string, TagVisitorCallback> 29 * @since 0.3.0 30 * 31 * @var array<non-empty-string, TagVisitorCallback> 31 32 */ 32 33 private $visitors = array(); … … 35 36 * Registers a tag visitor. 36 37 * 38 * @since 0.3.0 39 * 37 40 * @phpstan-param TagVisitorCallback $tag_visitor_callback 38 41 * 39 * @param string$id Identifier for the tag visitor.40 * @param callable $tag_visitor_callback Tag visitor callback.42 * @param non-empty-string $id Identifier for the tag visitor. 43 * @param callable $tag_visitor_callback Tag visitor callback. 41 44 */ 42 45 public function register( string $id, callable $tag_visitor_callback ): void { … … 47 50 * Determines if a visitor has been registered. 48 51 * 49 * @param string $id Identifier for the tag visitor. 52 * @since 0.3.0 53 * 54 * @param non-empty-string $id Identifier for the tag visitor. 50 55 * @return bool Whether registered. 51 56 */ … … 57 62 * Gets a registered visitor. 58 63 * 59 * @param string $id Identifier for the tag visitor. 64 * @since 0.3.0 65 * 66 * @param non-empty-string $id Identifier for the tag visitor. 60 67 * @return TagVisitorCallback|null Whether registered. 61 68 */ … … 70 77 * Unregisters a tag visitor. 71 78 * 72 * @param string $id Identifier for the tag visitor. 79 * @since 0.3.0 80 * 81 * @param non-empty-string $id Identifier for the tag visitor. 73 82 * @return bool Whether a tag visitor was unregistered. 74 83 */ … … 84 93 * Returns an iterator for the URL Metrics in the group. 85 94 * 95 * @since 0.3.0 96 * 86 97 * @return ArrayIterator<string, TagVisitorCallback> ArrayIterator for tag visitors. 87 98 */ … … 93 104 * Counts the URL Metrics in the group. 94 105 * 106 * @since 0.3.0 107 * 95 108 * @return int<0, max> URL Metric count. 96 109 */ -
optimization-detective/tags/1.0.0-beta2/class-od-url-metric-group-collection.php
r3229869 r3240307 19 19 * 20 20 * @since 0.1.0 21 * @access private22 21 */ 23 22 final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggregate, JsonSerializable { … … 32 31 * in this case, in which every single URL Metric is added. 33 32 * 33 * @since 0.1.0 34 34 * @var OD_URL_Metric_Group[] 35 35 * @phpstan-var non-empty-array<OD_URL_Metric_Group> … … 48 48 * Breakpoints in max widths. 49 49 * 50 * Valid values are from 1 to PHP_INT_MAX - 1. This is because: 51 * 52 * 1. It doesn't make sense for there to be a viewport width of zero, so the first breakpoint (max width) must be at least 1. 53 * 2. After the last breakpoint, the final breakpoint group is set to be spanning one plus the last breakpoint max width up 54 * until PHP_INT_MAX. So a breakpoint cannot be PHP_INT_MAX because then the minimum viewport width for the final group 55 * would end up being larger than PHP_INT_MAX. 50 * A breakpoint must be greater than zero because a viewport group's maximum viewport width has a minimum (inclusive) 51 * value of 1, and the breakpoints are used as the maximum viewport widths for the viewport groups, with the addition of 52 * a final viewport group which has a maximum viewport width of infinity. 56 53 * 57 54 * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a 58 55 * single group. 59 56 * 60 * @ var int[]61 * @ phpstan-var positive-int[]57 * @since 0.1.0 58 * @var positive-int[] 62 59 */ 63 60 private $breakpoints; … … 66 63 * Sample size for URL Metrics for a given breakpoint. 67 64 * 68 * @ var int69 * @ phpstan-var positive-int65 * @since 0.1.0 66 * @var int<1, max> 70 67 */ 71 68 private $sample_size; … … 76 73 * A freshness age of zero means a URL Metric will always be considered stale. 77 74 * 78 * @ var int79 * @ phpstan-var 0|positive-int75 * @since 0.1.0 76 * @var int<0, max> 80 77 */ 81 78 private $freshness_ttl; … … 84 81 * Result cache. 85 82 * 83 * @since 0.3.0 86 84 * @var array{ 87 85 * get_group_for_viewport_width?: array<int, OD_URL_Metric_Group>, … … 92 90 * get_common_lcp_element?: OD_Element|null, 93 91 * get_all_element_max_intersection_ratios?: array<string, float>, 94 * get_xpath_elements_map?: array<string, non-empty-array< int, OD_Element>>,92 * get_xpath_elements_map?: array<string, non-empty-array<non-negative-int, OD_Element>>, 95 93 * get_all_elements_positioned_in_any_initial_viewport?: array<string, bool>, 96 94 * } … … 101 99 * Constructor. 102 100 * 101 * @since 0.1.0 102 * 103 103 * @throws InvalidArgumentException When an invalid argument is supplied. 104 * 105 * @phpstan-param positive-int[] $breakpoints 106 * @phpstan-param int<1, max> $sample_size 107 * @phpstan-param int<0, max> $freshness_ttl 104 108 * 105 109 * @param OD_URL_Metric[] $url_metrics URL Metrics. … … 128 132 $breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) ); 129 133 foreach ( $breakpoints as $breakpoint ) { 130 if ( ! is_int( $breakpoint ) || $breakpoint < 1 || PHP_INT_MAX === $breakpoint) {134 if ( ! is_int( $breakpoint ) || $breakpoint < 1 ) { 131 135 throw new InvalidArgumentException( 132 136 esc_html( … … 134 138 /* translators: %d is the invalid breakpoint */ 135 139 __( 136 'Each of the breakpoints must be greater than zero and less than PHP_INT_MAX, but encountered: %d',140 'Each of the breakpoints must be greater than zero, but encountered: %d', 137 141 'optimization-detective' 138 142 ), … … 197 201 198 202 /** 203 * Gets the breakpoints in max widths. 204 * 205 * @since 1.0.0 206 * 207 * @return positive-int[] Breakpoints in max widths. 208 */ 209 public function get_breakpoints(): array { 210 return $this->breakpoints; 211 } 212 213 /** 214 * Gets the sample size for URL Metrics for a given breakpoint. 215 * 216 * @since 1.0.0 217 * 218 * @return int<1, max> Sample size for URL Metrics for a given breakpoint. 219 */ 220 public function get_sample_size(): int { 221 return $this->sample_size; 222 } 223 224 /** 225 * Gets the freshness age (TTL) for a given URL Metric.. 226 * 227 * @since 1.0.0 228 * 229 * @return int<0, max> Freshness age (TTL) for a given URL Metric. 230 */ 231 public function get_freshness_ttl(): int { 232 return $this->freshness_ttl; 233 } 234 235 /** 199 236 * Gets the first URL Metric group. 200 237 * … … 216 253 * This group normally represents viewports for desktop devices. This group always has a minimum viewport width 217 254 * defined as one greater than the largest breakpoint returned by {@see od_get_breakpoint_max_widths()}. 218 * The maximum viewport is always `PHP_INT_MAX`, or in other words it is unbounded.255 * The maximum viewport width of this group is always `null`, or in other words it is unbounded. 219 256 * 220 257 * @since 0.7.0 … … 245 282 */ 246 283 private function create_groups(): array { 247 $groups = array();248 $min_width = 0;249 foreach ( $this->breakpoints as $max_width ) {250 $groups[] = new OD_URL_Metric_Group( array(), $min_width, $max_width, $this->sample_size, $this->freshness_ttl, $this );251 $min_width = $max_width + 1;252 } 253 $groups[] = new OD_URL_Metric_Group( array(), $min_width , PHP_INT_MAX, $this->sample_size, $this->freshness_ttl, $this );284 $groups = array(); 285 $min_width_exclusive = 0; 286 foreach ( $this->breakpoints as $max_width_inclusive ) { 287 $groups[] = new OD_URL_Metric_Group( array(), $min_width_exclusive, $max_width_inclusive, $this->sample_size, $this->freshness_ttl, $this ); 288 $min_width_exclusive = $max_width_inclusive; 289 } 290 $groups[] = new OD_URL_Metric_Group( array(), $min_width_exclusive, null, $this->sample_size, $this->freshness_ttl, $this ); 254 291 return $groups; 255 292 } … … 273 310 } 274 311 // @codeCoverageIgnoreStart 275 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX.312 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to an unbounded maximum. 276 313 throw new InvalidArgumentException( 277 314 esc_html__( 'No group available to add URL Metric to.', 'optimization-detective' ) … … 286 323 * @throws InvalidArgumentException When there is no group for the provided viewport width. This would only happen if a negative width is provided. 287 324 * 288 * @param int $viewport_width Viewport width.325 * @param positive-int $viewport_width Viewport width. 289 326 * @return OD_URL_Metric_Group URL Metric group for the viewport width. 290 327 */ … … 301 338 } 302 339 // @codeCoverageIgnoreStart 303 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX.340 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to an unbounded maximum. 304 341 throw new InvalidArgumentException( 305 342 esc_html( … … 496 533 * @since 0.7.0 497 534 * 498 * @return array<string, non-empty-array< int, OD_Element>> Keys are XPaths and values are the element instances.535 * @return array<string, non-empty-array<non-negative-int, OD_Element>> Keys are XPaths and values are the element instances. 499 536 */ 500 537 public function get_xpath_elements_map(): array { … … 668 705 * groups: array<int, array{ 669 706 * lcp_element: ?OD_Element, 670 * minimum_viewport_width: 0|positive-int,671 * maximum_viewport_width: positive-int,707 * minimum_viewport_width: int<0, max>, 708 * maximum_viewport_width: int<1, max>|null, 672 709 * complete: bool, 673 710 * url_metrics: OD_URL_Metric[] -
optimization-detective/tags/1.0.0-beta2/class-od-url-metric-group.php
r3229869 r3240307 19 19 * 20 20 * @since 0.1.0 21 * @access private22 21 */ 23 22 final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSerializable { … … 33 32 34 33 /** 35 * Minimum possible viewport width for the group (inclusive). 36 * 37 * @since 0.1.0 38 * 39 * @var int 40 * @phpstan-var 0|positive-int 34 * Minimum possible viewport width for the group (exclusive). 35 * 36 * @since 0.1.0 37 * 38 * @var int<0, max> 41 39 */ 42 40 private $minimum_viewport_width; 43 41 44 42 /** 45 * Maximum possible viewport width for the group (inclusive). 46 * 47 * @since 0.1.0 48 * 49 * @var int 50 * @phpstan-var positive-int 43 * Maximum possible viewport width for the group (inclusive), where null means it is unbounded. 44 * 45 * @since 0.1.0 46 * 47 * @var int<1, max>|null 51 48 */ 52 49 private $maximum_viewport_width; … … 57 54 * @since 0.1.0 58 55 * 59 * @var int 60 * @phpstan-var positive-int 56 * @var int<1, max> 61 57 */ 62 58 private $sample_size; … … 67 63 * @since 0.1.0 68 64 * 69 * @var int 70 * @phpstan-var 0|positive-int 65 * @var int<0, max> 71 66 */ 72 67 private $freshness_ttl; … … 100 95 * This class should never be directly constructed. It should only be constructed by the {@see OD_URL_Metric_Group_Collection::create_groups()}. 101 96 * 97 * @since 0.1.0 98 * 102 99 * @access private 103 100 * @throws InvalidArgumentException If arguments are invalid. 104 101 * 102 * @phpstan-param int<0, max> $minimum_viewport_width 103 * @phpstan-param int<1, max>|null $maximum_viewport_width 104 * @phpstan-param int<1, max> $sample_size 105 * @phpstan-param int<0, max> $freshness_ttl 106 * 105 107 * @param OD_URL_Metric[] $url_metrics URL Metrics to add to the group. 106 * @param int $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.107 * @param int $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.108 * @param int $minimum_viewport_width Minimum possible viewport width (exclusive) for the group. Must be zero or greater. 109 * @param int|null $maximum_viewport_width Maximum possible viewport width (inclusive) for the group. Must be greater than zero and the minimum viewport width. Null means unbounded. 108 110 * @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints. 109 111 * @param int $freshness_ttl Freshness age (TTL) for a given URL Metric. 110 112 * @param OD_URL_Metric_Group_Collection $collection Collection that this instance belongs to. 111 113 */ 112 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 ) {114 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 ) { 113 115 if ( $minimum_viewport_width < 0 ) { 114 116 throw new InvalidArgumentException( … … 116 118 ); 117 119 } 118 if ( $maximum_viewport_width < 1 ) { 119 throw new InvalidArgumentException( 120 esc_html__( 'The maximum viewport width must be greater than zero.', 'optimization-detective' ) 121 ); 122 } 123 if ( $minimum_viewport_width >= $maximum_viewport_width ) { 124 throw new InvalidArgumentException( 125 esc_html__( 'The minimum viewport width must be smaller than the maximum viewport width.', 'optimization-detective' ) 126 ); 120 if ( isset( $maximum_viewport_width ) ) { 121 if ( $maximum_viewport_width < 1 ) { 122 throw new InvalidArgumentException( 123 esc_html__( 'The maximum viewport width must be greater than zero.', 'optimization-detective' ) 124 ); 125 } 126 if ( $minimum_viewport_width >= $maximum_viewport_width ) { 127 throw new InvalidArgumentException( 128 esc_html__( 'The minimum viewport width must be smaller than the maximum viewport width.', 'optimization-detective' ) 129 ); 130 } 127 131 } 128 132 $this->minimum_viewport_width = $minimum_viewport_width; … … 159 163 160 164 /** 161 * Gets the minimum possible viewport width ( inclusive).165 * Gets the minimum possible viewport width (exclusive). 162 166 * 163 167 * @since 0.1.0 164 168 * 165 169 * @todo Eliminate in favor of readonly public property. 166 * @return int<0, max> Minimum viewport width .170 * @return int<0, max> Minimum viewport width (exclusive). 167 171 */ 168 172 public function get_minimum_viewport_width(): int { … … 176 180 * 177 181 * @todo Eliminate in favor of readonly public property. 178 * @return int<1, max> Minimum viewport width.179 */ 180 public function get_maximum_viewport_width(): int {182 * @return int<1, max>|null Minimum viewport width (inclusive). Null means unbounded. 183 */ 184 public function get_maximum_viewport_width(): ?int { 181 185 return $this->maximum_viewport_width; 182 186 } … … 188 192 * 189 193 * @todo Eliminate in favor of readonly public property. 190 * @phpstan-return positive-int 191 * @return int Sample size. 194 * @return int<1, max> Sample size. 192 195 */ 193 196 public function get_sample_size(): int { … … 201 204 * 202 205 * @todo Eliminate in favor of readonly public property. 203 * @phpstan-return 0|positive-int 204 * @return int Freshness age. 206 * @return int<0, max> Freshness age. 205 207 */ 206 208 public function get_freshness_ttl(): int { … … 209 211 210 212 /** 211 * Checks whether the provided viewport width is within the minimum/maximum range for. 212 * 213 * @since 0.1.0 213 * Gets the collection that this group is a part of. 214 * 215 * @since 1.0.0 216 * 217 * @todo Eliminate in favor of readonly public property. 218 * @return OD_URL_Metric_Group_Collection Collection. 219 */ 220 public function get_collection(): OD_URL_Metric_Group_Collection { 221 return $this->collection; 222 } 223 224 /** 225 * Checks whether the provided viewport width is between the minimum (exclusive) and maximum (inclusive). 226 * 227 * @since 0.1.0 228 * 229 * @phpstan-param int<1, max> $viewport_width 214 230 * 215 231 * @param int $viewport_width Viewport width. … … 218 234 public function is_viewport_width_in_range( int $viewport_width ): bool { 219 235 return ( 220 $viewport_width > =$this->minimum_viewport_width &&221 $viewport_width <= $this->maximum_viewport_width236 $viewport_width > $this->minimum_viewport_width && 237 ( null === $this->maximum_viewport_width || $viewport_width <= $this->maximum_viewport_width ) 222 238 ); 223 239 } … … 287 303 } 288 304 289 // The ETag is not populated yet, so this is stale. Eventually this will be required.290 if ( $url_metric->get_etag() === null ) {291 return false;292 }293 294 305 // The ETag of the URL Metric does not match the current ETag for the collection, so it is stale. 295 306 if ( ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) ) { … … 330 341 * Seen breadcrumbs counts. 331 342 * 332 * @var array<int, string> $seen_breadcrumbs343 * @var array<int, non-empty-string> $seen_breadcrumbs 333 344 */ 334 345 $seen_breadcrumbs = array(); … … 337 348 * Breadcrumb counts. 338 349 * 339 * @var array<int, int> $breadcrumb_counts350 * @var array<int, non-negative-int> $breadcrumb_counts 340 351 */ 341 352 $breadcrumb_counts = array(); … … 490 501 * freshness_ttl: 0|positive-int, 491 502 * sample_size: positive-int, 492 * minimum_viewport_width: 0|positive-int,493 * maximum_viewport_width: positive-int,503 * minimum_viewport_width: int<0, max>, 504 * maximum_viewport_width: int<1, max>|null, 494 505 * lcp_element: ?OD_Element, 495 506 * complete: bool, -
optimization-detective/tags/1.0.0-beta2/class-od-url-metric.php
r3229869 r3240307 17 17 * 18 18 * @phpstan-type ViewportRect array{ 19 * width: int,20 * height: int19 * width: positive-int, 20 * height: positive-int 21 21 * } 22 22 * @phpstan-type DOMRect array{ … … 40 40 * @phpstan-type Data array{ 41 41 * uuid: non-empty-string, 42 * etag ?: non-empty-string,42 * etag: non-empty-string, 43 43 * url: non-empty-string, 44 44 * timestamp: float, … … 61 61 * 62 62 * @since 0.1.0 63 * @access private64 63 */ 65 64 class OD_URL_Metric implements JsonSerializable { … … 68 67 * Data. 69 68 * 69 * @since 0.1.0 70 70 * @var Data 71 71 */ … … 75 75 * Elements. 76 76 * 77 * @since 0.7.0 77 78 * @var OD_Element[] 78 79 */ … … 89 90 /** 90 91 * Constructor. 92 * 93 * @since 0.1.0 91 94 * 92 95 * @phpstan-param Data|array<string, mixed> $data Valid data or invalid data (in which case an exception is thrown). … … 105 108 /** 106 109 * Prepares data with validation and sanitization. 110 * 111 * @since 0.6.0 107 112 * 108 113 * @throws OD_Data_Validation_Exception When the input is invalid. … … 172 177 * @since 0.1.0 173 178 * @since 0.9.0 Added the 'etag' property to the schema. 179 * @since 1.0.0 The 'etag' property is now required. 174 180 * 175 181 * @todo Cache the return value? … … 231 237 'minLength' => 32, 232 238 'maxLength' => 32, 233 'required' => false, // To be made required in a future release.239 'required' => true, 234 240 'readonly' => true, // Omit from REST API. 235 241 ), … … 249 255 'type' => 'integer', 250 256 'required' => true, 251 'minimum' => 0,257 'minimum' => 1, 252 258 ), 253 259 'height' => array( 254 260 'type' => 'integer', 255 261 'required' => true, 256 'minimum' => 0,262 'minimum' => 1, 257 263 ), 258 264 ), … … 350 356 * @param array<string, mixed> $additional_properties Additional properties. 351 357 * @param string $filter_name Filter name used to extend. 352 *353 358 * @return array<string, mixed> Extended schema. 354 359 */ … … 437 442 * @since 0.6.0 438 443 * 439 * @return string UUID.444 * @return non-empty-string UUID. 440 445 */ 441 446 public function get_uuid(): string { … … 447 452 * 448 453 * @since 0.9.0 449 * 450 * @return non-empty-string|null ETag.451 * /452 public function get_etag(): ?string {453 // Since the ETag is optional for now, return null for old URL Metrics that do not have one.454 return $this->data['etag'] ?? null;454 * @since 1.0.0 No longer returns null as 'etag' is now required. 455 * 456 * @return non-empty-string ETag. 457 */ 458 public function get_etag(): string { 459 return $this->data['etag']; 455 460 } 456 461 … … 460 465 * @since 0.1.0 461 466 * 462 * @return string URL.467 * @return non-empty-string URL. 463 468 */ 464 469 public function get_url(): string { … … 482 487 * @since 0.1.0 483 488 * 484 * @return int Viewport width.489 * @return positive-int Viewport width. 485 490 */ 486 491 public function get_viewport_width(): int { -
optimization-detective/tags/1.0.0-beta2/class-od-visited-tag-state.php
r3229869 r3240307 31 31 /** 32 32 * Constructor. 33 * 34 * @since 1.0.0 33 35 */ 34 36 public function __construct() { -
optimization-detective/tags/1.0.0-beta2/detect.js
r3229869 r3240307 97 97 98 98 /** 99 * Checks whether the URL Metric(s) for the provided viewport width is needed. 99 * Gets the status for the URL Metric group for the provided viewport width. 100 * 101 * The comparison logic here corresponds with the PHP logic in `OD_URL_Metric_Group::is_viewport_width_in_range()`. 102 * This function is also similar to the PHP logic in `\OD_URL_Metric_Group_Collection::get_group_for_viewport_width()`. 100 103 * 101 104 * @param {number} viewportWidth - Current viewport width. 102 105 * @param {URLMetricGroupStatus[]} urlMetricGroupStatuses - Viewport group statuses. 103 * @return {boolean} Whether URL Metrics are needed. 104 */ 105 function isViewportNeeded( viewportWidth, urlMetricGroupStatuses ) { 106 let lastWasLacking = false; 107 for ( const { minimumViewportWidth, complete } of urlMetricGroupStatuses ) { 108 if ( viewportWidth >= minimumViewportWidth ) { 109 lastWasLacking = ! complete; 110 } else { 111 break; 112 } 113 } 114 return lastWasLacking; 106 * @return {URLMetricGroupStatus} The URL metric group for the viewport width. 107 */ 108 function getGroupForViewportWidth( viewportWidth, urlMetricGroupStatuses ) { 109 for ( const urlMetricGroupStatus of urlMetricGroupStatuses ) { 110 if ( 111 viewportWidth > urlMetricGroupStatus.minimumViewportWidth && 112 ( null === urlMetricGroupStatus.maximumViewportWidth || 113 viewportWidth <= urlMetricGroupStatus.maximumViewportWidth ) 114 ) { 115 return urlMetricGroupStatus; 116 } 117 } 118 throw new Error( 119 `${ consoleLogPrefix } Unexpectedly unable to locate group for the current viewport width.` 120 ); 121 } 122 123 /** 124 * Gets the sessionStorage key for keeping track of whether the current client session already submitted a URL Metric. 125 * 126 * @param {string} currentETag - Current ETag. 127 * @param {string} currentUrl - Current URL. 128 * @param {URLMetricGroupStatus} urlMetricGroupStatus - URL Metric group status. 129 * @return {Promise<string>} Session storage key. 130 */ 131 async function getAlreadySubmittedSessionStorageKey( 132 currentETag, 133 currentUrl, 134 urlMetricGroupStatus 135 ) { 136 const message = [ 137 currentETag, 138 currentUrl, 139 urlMetricGroupStatus.minimumViewportWidth, 140 urlMetricGroupStatus.maximumViewportWidth || '', 141 ].join( '-' ); 142 143 /* 144 * Note that the components are hashed for a couple of reasons: 145 * 146 * 1. It results in a consistent length string devoid of any special characters that could cause problems. 147 * 2. Since the key includes the URL, hashing it avoids potential privacy concerns where the sessionStorage is 148 * examined to see which URLs the client went to. 149 * 150 * The SHA-1 algorithm is chosen since it is the fastest and there is no need for cryptographic security. 151 */ 152 const msgBuffer = new TextEncoder().encode( message ); 153 const hashBuffer = await crypto.subtle.digest( 'SHA-1', msgBuffer ); 154 const hashHex = Array.from( new Uint8Array( hashBuffer ) ) 155 .map( ( b ) => b.toString( 16 ).padStart( 2, '0' ) ) 156 .join( '' ); 157 return `odSubmitted-${ hashHex }`; 115 158 } 116 159 … … 254 297 * @param {boolean} args.isDebug Whether to show debug messages. 255 298 * @param {string} args.restApiEndpoint URL for where to send the detection data. 299 * @param {string} [args.restApiNonce] Nonce for the REST API when the user is logged-in. 256 300 * @param {string} args.currentETag Current ETag. 257 301 * @param {string} args.currentUrl Current URL. … … 261 305 * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses. 262 306 * @param {number} args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock. 307 * @param {number} args.freshnessTTL The freshness age (TTL) for a given URL Metric. 263 308 * @param {string} args.webVitalsLibrarySrc The URL for the web-vitals library. 264 309 * @param {CollectionDebugData} [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode. … … 270 315 extensionModuleUrls, 271 316 restApiEndpoint, 317 restApiNonce, 272 318 currentETag, 273 319 currentUrl, … … 277 323 urlMetricGroupStatuses, 278 324 storageLockTTL, 325 freshnessTTL, 279 326 webVitalsLibrarySrc, 280 327 urlMetricGroupCollection, … … 298 345 } 299 346 347 if ( win.innerWidth === 0 || win.innerHeight === 0 ) { 348 if ( isDebug ) { 349 log( 350 'Window must have non-zero dimensions for URL Metric collection.' 351 ); 352 } 353 return; 354 } 355 300 356 // Abort if the current viewport is not among those which need URL Metrics. 301 if ( ! isViewportNeeded( win.innerWidth, urlMetricGroupStatuses ) ) { 357 const urlMetricGroupStatus = getGroupForViewportWidth( 358 win.innerWidth, 359 urlMetricGroupStatuses 360 ); 361 if ( urlMetricGroupStatus.complete ) { 302 362 if ( isDebug ) { 303 363 log( 'No need for URL Metrics from the current viewport.' ); 304 364 } 305 365 return; 366 } 367 368 // Abort if the client already submitted a URL Metric for this URL and viewport group. 369 const alreadySubmittedSessionStorageKey = 370 await getAlreadySubmittedSessionStorageKey( 371 currentETag, 372 currentUrl, 373 urlMetricGroupStatus 374 ); 375 if ( alreadySubmittedSessionStorageKey in sessionStorage ) { 376 const previousVisitTime = parseInt( 377 sessionStorage.getItem( alreadySubmittedSessionStorageKey ), 378 10 379 ); 380 if ( 381 ! isNaN( previousVisitTime ) && 382 ( getCurrentTime() - previousVisitTime ) / 1000 < freshnessTTL 383 ) { 384 if ( isDebug ) { 385 log( 386 'The current client session already submitted a fresh URL Metric for this URL so a new one will not be collected now.' 387 ); 388 return; 389 } 390 } 306 391 } 307 392 … … 604 689 } 605 690 691 // Finalize extensions. 606 692 if ( extensions.size > 0 ) { 607 693 /** @type {Promise[]} */ … … 656 742 } 657 743 744 /* 745 * Now prepare the URL Metric to be sent as JSON request body. 746 */ 747 748 const maxBodyLengthKiB = 64; 749 const maxBodyLengthBytes = maxBodyLengthKiB * 1024; 750 751 // TODO: Consider adding replacer to reduce precision on numbers in DOMRect to reduce payload size. 752 const jsonBody = JSON.stringify( urlMetric ); 753 const percentOfBudget = 754 ( jsonBody.length / ( maxBodyLengthKiB * 1000 ) ) * 100; 755 756 /* 757 * According to the fetch() spec: 758 * "If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error." 759 * This is what browsers also implement for navigator.sendBeacon(). Therefore, if the size of the JSON is greater 760 * than the maximum, we should avoid even trying to send it. 761 */ 762 if ( jsonBody.length > maxBodyLengthBytes ) { 763 if ( isDebug ) { 764 error( 765 `Unable to send URL Metric because it is ${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( 766 percentOfBudget 767 ) }% of ${ maxBodyLengthKiB } KiB limit:`, 768 urlMetric 769 ); 770 } 771 return; 772 } 773 658 774 // Even though the server may reject the REST API request, we still have to set the storage lock 659 775 // because we can't look at the response when sending a beacon. 660 776 setStorageLock( getCurrentTime() ); 661 777 778 // Remember that the URL Metric was submitted for this URL to avoid having multiple entries submitted by the same client. 779 sessionStorage.setItem( 780 alreadySubmittedSessionStorageKey, 781 String( getCurrentTime() ) 782 ); 783 662 784 if ( isDebug ) { 663 log( 'Sending URL Metric:', urlMetric ); 785 const message = `Sending URL Metric (${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( 786 percentOfBudget 787 ) }% of ${ maxBodyLengthKiB } KiB limit):`; 788 789 // The threshold of 50% is used because the limit for all beacons combined is 64 KiB, not just the data for one beacon. 790 if ( percentOfBudget < 50 ) { 791 log( message, urlMetric ); 792 } else { 793 warn( message, urlMetric ); 794 } 664 795 } 665 796 666 797 const url = new URL( restApiEndpoint ); 798 if ( typeof restApiNonce === 'string' ) { 799 url.searchParams.set( '_wpnonce', restApiNonce ); 800 } 667 801 url.searchParams.set( 'slug', urlMetricSlug ); 668 802 url.searchParams.set( 'current_etag', currentETag ); … … 676 810 navigator.sendBeacon( 677 811 url, 678 new Blob( [ JSON.stringify( urlMetric )], {812 new Blob( [ jsonBody ], { 679 813 type: 'application/json', 680 814 } ) -
optimization-detective/tags/1.0.0-beta2/detect.min.js
r3210025 r3240307 1 const win=window,doc=win.document,consoleLogPrefix="[Optimization Detective]",storageLockTimeSessionKey="odStorageLockTime";function isStorageLocked(e,t){if(0===t)return!1;try{const o=parseInt(sessionStorage.getItem("odStorageLockTime"));return!isNaN(o)&&e<o+1e3*t}catch(e){return!1}}function setStorageLock(e){try{sessionStorage.setItem("odStorageLockTime",String(e))}catch(e){}}function log(...e){console.log(consoleLogPrefix,...e)}function warn(...e){console.warn(consoleLogPrefix,...e)}function error(...e){console.error(consoleLogPrefix,...e)}function isViewportNeeded(e,t){let o=!1;for(const{minimumViewportWidth:n,complete:r}of t){if(!(e>=n))break;o=!r}return o}function getCurrentTime(){return Date.now()}function recursiveFreeze(e){for(const t of Object.getOwnPropertyNames(e)){const o=e[t];null!==o&&"object"==typeof o&&recursiveFreeze(o)}Object.freeze(e)}let urlMetric;const reservedRootPropertyKeys=new Set(["url","viewport","elements"]);function getRootData(){const e=structuredClone(urlMetric);return recursiveFreeze(e),e}function extendRootData(e){for(const t of Object.getOwnPropertyNames(e))if(reservedRootPropertyKeys.has(t))throw new Error(`Disallowed setting of key '${t}' on root.`);Object.assign(urlMetric,e)}const elementsByXPath=new Map,reservedElementPropertyKeys=new Set(["isLCP","isLCPCandidate","xpath","intersectionRatio","intersectionRect","boundingClientRect"]);function getElementData(e){const t=elementsByXPath.get(e);if(t){const e=structuredClone(t);return recursiveFreeze(e),e}return null}function extendElementData(e,t){if(!elementsByXPath.has(e))throw new Error(`Unknown element with XPath: ${e}`);for(const e of Object.getOwnPropertyNames(t))if(reservedElementPropertyKeys.has(e))throw new Error(`Disallowed setting of key '${e}' on element.`);const o=elementsByXPath.get(e);Object.assign(o,t)}export default async function detect({minViewportAspectRatio:e,maxViewportAspectRatio:t,isDebug:o,extensionModuleUrls:n,restApiEndpoint:r,currentETag:i,currentUrl:s,urlMetricSlug:a,cachePurgePostId:c,urlMetricHMAC:l,urlMetricGroupStatuses:d,storageLockTTL:u,webVitalsLibrarySrc:g,urlMetricGroupCollection:f}){if(o){const e=[];for(const t of f.groups)for(const o of t.url_metrics)o.creationDate=new Date(1e3*o.timestamp),e.push(o);log("Stored URL Metric Group Collection:",f),e.sort(((e,t)=>t.timestamp-e.timestamp)),log("Stored URL Metrics in reverse chronological order:",e)}if(!isViewportNeeded(win.innerWidth,d))return void(o&&log("No need for URL Metrics from the current viewport."));const p=win.innerWidth/win.innerHeight;if(p<e||p>t)return void(o&&warn(`Viewport aspect ratio (${p}) is not in the accepted range of ${e} to ${t}.`));if(await new Promise((e=>{"loading"!==doc.readyState?e():doc.addEventListener("DOMContentLoaded",e,{once:!0})})),await new Promise((e=>{"complete"===doc.readyState?e():win.addEventListener("load",e,{once:!0})})),"function"==typeof requestIdleCallback&&await new Promise((e=>{requestIdleCallback(e)})),isStorageLocked(getCurrentTime(),u))return void(o&&warn("Aborted detection due to storage being locked."));let w=!1;window.addEventListener("resize",(()=>{w=!0}),{once:!0});const{onTTFB:m,onFCP:h,onLCP:P,onINP:L,onCLS:y}=await import(g);if(doc.documentElement.scrollTop>0)return void(o&&warn("Aborted detection since initial scroll position of page is not at the top."));o&&log("Proceeding with detection");const v=new Map,b=[],S=[];for(const e of n)try{const t=await import(e);if(v.set(e,t),t.initialize instanceof Function){const n=t.initialize({isDebug:o,onTTFB:m,onFCP:h,onLCP:P,onINP:L,onCLS:y});n instanceof Promise&&(b.push(n),S.push(e))}}catch(t){error(`Failed to start initializing extension '${e}':`,t)}const C=await Promise.allSettled(b);for(const[e,t]of C.entries())"rejected"===t.status&&error(`Failed to initialize extension '${S[e]}':`,t.reason);const R=doc.body.querySelectorAll("[data-od-xpath]"),M=new Map([...R].map((e=>[e,e.dataset.odXpath]))),D=[];let E;function x(){E instanceof IntersectionObserver&&(E.disconnect(),win.removeEventListener("scroll",x))}M.size>0&&(await new Promise((e=>{E=new IntersectionObserver((t=>{for(const e of t)D.push(e);e()}),{root:null,threshold:0});for(const e of M.keys())E.observe(e)})),win.addEventListener("scroll",x,{once:!0,passive:!0}));const k=[];await new Promise((e=>{P((t=>{k.push(t),e()}),{reportAllChanges:!0})})),x(),o&&log("Detection is stopping."),urlMetric={url:s,viewport:{width:win.innerWidth,height:win.innerHeight},elements:[]};const z=k.at(-1);for(const e of D){const t=M.get(e.target);if(!t){o&&error("Unable to look up XPath for element");continue}const n=z?.entries[0]?.element,r={isLCP:e.target===n,isLCPCandidate:!!k.find((t=>{const o=t.entries[0]?.element;return o===e.target})),xpath:t,intersectionRatio:e.intersectionRatio,intersectionRect:e.intersectionRect,boundingClientRect:e.boundingClientRect};urlMetric.elements.push(r),elementsByXPath.set(r.xpath,r)}if(o&&log("Current URL Metric:",urlMetric),await new Promise((e=>{win.addEventListener("pagehide",e,{once:!0}),win.addEventListener("pageswap",e,{once:!0}),doc.addEventListener("visibilitychange",(()=>{"hidden"===document.visibilityState&&e()}),{once:!0})})),w)return void(o&&log("Aborting URL Metric collection due to viewport size change."));if(v.size>0){const e=[],t=[];for(const[n,r]of v.entries())if(r.finalize instanceof Function)try{const i=r.finalize({isDebug:o,getRootData,getElementData,extendElementData,extendRootData});i instanceof Promise&&(e.push(i),t.push(n))}catch(e){error(`Unable to start finalizing extension '${n}':`,e)}const n=await Promise.allSettled(e);for(const[e,o]of n.entries())"rejected"===o.status&&error(`Failed to finalize extension '${t[e]}':`,o.reason)}setStorageLock(getCurrentTime()),o&&log("Sending URL Metric:",urlMetric);const T=new URL(r);T.searchParams.set("slug",a),T.searchParams.set("current_etag",i),"number"==typeof c&&T.searchParams.set("cache_purge_post_id",c.toString()),T.searchParams.set("hmac",l),navigator.sendBeacon(T,new Blob([JSON.stringify(urlMetric)],{type:"application/json"})),M.clear()}1 const win=window,doc=win.document,consoleLogPrefix="[Optimization Detective]",storageLockTimeSessionKey="odStorageLockTime";function isStorageLocked(e,t){if(0===t)return!1;try{const o=parseInt(sessionStorage.getItem("odStorageLockTime"));return!isNaN(o)&&e<o+1e3*t}catch(e){return!1}}function setStorageLock(e){try{sessionStorage.setItem("odStorageLockTime",String(e))}catch(e){}}function log(...e){console.log(consoleLogPrefix,...e)}function warn(...e){console.warn(consoleLogPrefix,...e)}function error(...e){console.error(consoleLogPrefix,...e)}function getGroupForViewportWidth(e,t){for(const o of t)if(e>o.minimumViewportWidth&&(null===o.maximumViewportWidth||e<=o.maximumViewportWidth))return o;throw new Error(`${consoleLogPrefix} Unexpectedly unable to locate group for the current viewport width.`)}async function getAlreadySubmittedSessionStorageKey(e,t,o){const n=[e,t,o.minimumViewportWidth,o.maximumViewportWidth||""].join("-"),r=(new TextEncoder).encode(n),i=await crypto.subtle.digest("SHA-1",r);return`odSubmitted-${Array.from(new Uint8Array(i)).map((e=>e.toString(16).padStart(2,"0"))).join("")}`}function getCurrentTime(){return Date.now()}function recursiveFreeze(e){for(const t of Object.getOwnPropertyNames(e)){const o=e[t];null!==o&&"object"==typeof o&&recursiveFreeze(o)}Object.freeze(e)}let urlMetric;const reservedRootPropertyKeys=new Set(["url","viewport","elements"]);function getRootData(){const e=structuredClone(urlMetric);return recursiveFreeze(e),e}function extendRootData(e){for(const t of Object.getOwnPropertyNames(e))if(reservedRootPropertyKeys.has(t))throw new Error(`Disallowed setting of key '${t}' on root.`);Object.assign(urlMetric,e)}const elementsByXPath=new Map,reservedElementPropertyKeys=new Set(["isLCP","isLCPCandidate","xpath","intersectionRatio","intersectionRect","boundingClientRect"]);function getElementData(e){const t=elementsByXPath.get(e);if(t){const e=structuredClone(t);return recursiveFreeze(e),e}return null}function extendElementData(e,t){if(!elementsByXPath.has(e))throw new Error(`Unknown element with XPath: ${e}`);for(const e of Object.getOwnPropertyNames(t))if(reservedElementPropertyKeys.has(e))throw new Error(`Disallowed setting of key '${e}' on element.`);const o=elementsByXPath.get(e);Object.assign(o,t)}export default async function detect({minViewportAspectRatio:e,maxViewportAspectRatio:t,isDebug:o,extensionModuleUrls:n,restApiEndpoint:r,restApiNonce:i,currentETag:s,currentUrl:a,urlMetricSlug:c,cachePurgePostId:l,urlMetricHMAC:d,urlMetricGroupStatuses:u,storageLockTTL:g,freshnessTTL:f,webVitalsLibrarySrc:m,urlMetricGroupCollection:w}){if(o){const e=[];for(const t of w.groups)for(const o of t.url_metrics)o.creationDate=new Date(1e3*o.timestamp),e.push(o);log("Stored URL Metric Group Collection:",w),e.sort(((e,t)=>t.timestamp-e.timestamp)),log("Stored URL Metrics in reverse chronological order:",e)}if(0===win.innerWidth||0===win.innerHeight)return void(o&&log("Window must have non-zero dimensions for URL Metric collection."));const p=getGroupForViewportWidth(win.innerWidth,u);if(p.complete)return void(o&&log("No need for URL Metrics from the current viewport."));const h=await getAlreadySubmittedSessionStorageKey(s,a,p);if(h in sessionStorage){const e=parseInt(sessionStorage.getItem(h),10);if(!isNaN(e)&&(getCurrentTime()-e)/1e3<f&&o)return void log("The current client session already submitted a fresh URL Metric for this URL so a new one will not be collected now.")}const y=win.innerWidth/win.innerHeight;if(y<e||y>t)return void(o&&warn(`Viewport aspect ratio (${y}) is not in the accepted range of ${e} to ${t}.`));if(await new Promise((e=>{"loading"!==doc.readyState?e():doc.addEventListener("DOMContentLoaded",e,{once:!0})})),await new Promise((e=>{"complete"===doc.readyState?e():win.addEventListener("load",e,{once:!0})})),"function"==typeof requestIdleCallback&&await new Promise((e=>{requestIdleCallback(e)})),isStorageLocked(getCurrentTime(),g))return void(o&&warn("Aborted detection due to storage being locked."));let L=!1;window.addEventListener("resize",(()=>{L=!0}),{once:!0});const{onTTFB:P,onFCP:S,onLCP:b,onINP:v,onCLS:M}=await import(m);if(doc.documentElement.scrollTop>0)return void(o&&warn("Aborted detection since initial scroll position of page is not at the top."));o&&log("Proceeding with detection");const C=new Map,R=[],x=[];for(const e of n)try{const t=await import(e);if(C.set(e,t),t.initialize instanceof Function){const n=t.initialize({isDebug:o,onTTFB:P,onFCP:S,onLCP:b,onINP:v,onCLS:M});n instanceof Promise&&(R.push(n),x.push(e))}}catch(t){error(`Failed to start initializing extension '${e}':`,t)}const E=await Promise.allSettled(R);for(const[e,t]of E.entries())"rejected"===t.status&&error(`Failed to initialize extension '${x[e]}':`,t.reason);const D=doc.body.querySelectorAll("[data-od-xpath]"),T=new Map([...D].map((e=>[e,e.dataset.odXpath]))),z=[];let U;function k(){U instanceof IntersectionObserver&&(U.disconnect(),win.removeEventListener("scroll",k))}T.size>0&&(await new Promise((e=>{U=new IntersectionObserver((t=>{for(const e of t)z.push(e);e()}),{root:null,threshold:0});for(const e of T.keys())U.observe(e)})),win.addEventListener("scroll",k,{once:!0,passive:!0}));const $=[];await new Promise((e=>{b((t=>{$.push(t),e()}),{reportAllChanges:!0})})),k(),o&&log("Detection is stopping."),urlMetric={url:a,viewport:{width:win.innerWidth,height:win.innerHeight},elements:[]};const A=$.at(-1);for(const e of z){const t=T.get(e.target);if(!t){o&&error("Unable to look up XPath for element");continue}const n=A?.entries[0]?.element,r={isLCP:e.target===n,isLCPCandidate:!!$.find((t=>{const o=t.entries[0]?.element;return o===e.target})),xpath:t,intersectionRatio:e.intersectionRatio,intersectionRect:e.intersectionRect,boundingClientRect:e.boundingClientRect};urlMetric.elements.push(r),elementsByXPath.set(r.xpath,r)}if(o&&log("Current URL Metric:",urlMetric),await new Promise((e=>{win.addEventListener("pagehide",e,{once:!0}),win.addEventListener("pageswap",e,{once:!0}),doc.addEventListener("visibilitychange",(()=>{"hidden"===document.visibilityState&&e()}),{once:!0})})),L)return void(o&&log("Aborting URL Metric collection due to viewport size change."));if(C.size>0){const e=[],t=[];for(const[n,r]of C.entries())if(r.finalize instanceof Function)try{const i=r.finalize({isDebug:o,getRootData,getElementData,extendElementData,extendRootData});i instanceof Promise&&(e.push(i),t.push(n))}catch(e){error(`Unable to start finalizing extension '${n}':`,e)}const n=await Promise.allSettled(e);for(const[e,o]of n.entries())"rejected"===o.status&&error(`Failed to finalize extension '${t[e]}':`,o.reason)}const F=JSON.stringify(urlMetric),O=F.length/64e3*100;if(F.length>65536)return void(o&&error(`Unable to send URL Metric because it is ${F.length.toLocaleString()} bytes, ${Math.round(O)}% of 64 KiB limit:`,urlMetric));if(setStorageLock(getCurrentTime()),sessionStorage.setItem(h,String(getCurrentTime())),o){const e=`Sending URL Metric (${F.length.toLocaleString()} bytes, ${Math.round(O)}% of 64 KiB limit):`;O<50?log(e,urlMetric):warn(e,urlMetric)}const I=new URL(r);"string"==typeof i&&I.searchParams.set("_wpnonce",i),I.searchParams.set("slug",c),I.searchParams.set("current_etag",s),"number"==typeof l&&I.searchParams.set("cache_purge_post_id",l.toString()),I.searchParams.set("hmac",d),navigator.sendBeacon(I,new Blob([F],{type:"application/json"})),T.clear()} -
optimization-detective/tags/1.0.0-beta2/detection.php
r3229869 r3240307 39 39 * @global WP_Query $wp_query WordPress Query object. 40 40 * 41 * @return int|null Post ID or null if none found.41 * @return positive-int|null Post ID or null if none found. 42 42 */ 43 43 function od_get_cache_purge_post_id(): ?int { 44 44 $queried_object = get_queried_object(); 45 if ( $queried_object instanceof WP_Post ) {45 if ( $queried_object instanceof WP_Post && $queried_object->ID > 0 ) { 46 46 return $queried_object->ID; 47 47 } … … 56 56 && 57 57 $wp_query->posts[0] instanceof WP_Post 58 && 59 $wp_query->posts[0]->ID > 0 58 60 ) { 59 61 return $wp_query->posts[0]->ID; … … 69 71 * @access private 70 72 * 71 * @param string$slug URL Metrics slug.73 * @param non-empty-string $slug URL Metrics slug. 72 74 * @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection. 73 75 */ … … 129 131 static function ( OD_URL_Metric_Group $group ): array { 130 132 return array( 131 'minimumViewportWidth' => $group->get_minimum_viewport_width(), 133 'minimumViewportWidth' => $group->get_minimum_viewport_width(), // Exclusive. 134 'maximumViewportWidth' => $group->get_maximum_viewport_width(), // Inclusive. 132 135 'complete' => $group->is_complete(), 133 136 ); … … 136 139 ), 137 140 'storageLockTTL' => OD_Storage_Lock::get_ttl(), 141 'freshnessTTL' => od_get_url_metric_freshness_ttl(), 138 142 'webVitalsLibrarySrc' => $web_vitals_lib_src, 139 143 ); 144 if ( is_user_logged_in() ) { 145 $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); 146 } 140 147 if ( WP_DEBUG ) { 141 148 $detect_args['urlMetricGroupCollection'] = $group_collection; -
optimization-detective/tags/1.0.0-beta2/docs/extensions.md
r3229869 r3240307 11 11 **[Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) ([GitHub](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer)):** 12 12 13 1. Add breakpoint-specific `fetchpriority=high` preload links ( `LINK[rel=preload]`) for image URLs of LCP elements:13 1. Add breakpoint-specific `fetchpriority=high` preload links (both as `LINK[rel=preload]` elements and `Link` response headers) for image URLs of LCP elements: 14 14 1. An `IMG` element, including the `srcset`/`sizes` attributes supplied as `imagesrcset`/`imagesizes` on the `LINK`. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L167-L177), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L304-L349)) 15 15 2. The first `SOURCE` element with a `type` attribute in a `PICTURE` element. (Art-directed `PICTURE` elements using media queries are not supported.) ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L192-L275), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L304-L349)) … … 23 23 2. Implement lazy loading of CSS background images added via inline `style` attributes. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php#L205-L238), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L365-L380), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/lazy-load-bg-image.js)) 24 24 3. Lazy-load `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php#L163-L246), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L365-L380), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/lazy-load-video.js)) 25 5. Ensure that [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is added to all lazy-loaded `IMG` elements. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L148-L163)) 25 5. Responsive image sizes: 26 1. Compute the `sizes` attribute using the widths of an image collected from URL Metrics for each breakpoint (when not lazy-loaded since then handled by `sizes=auto`). ([1](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L170-L184), [2](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L412-L444)) 27 2. Ensure [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is set on `IMG` tags after setting correct lazy-loading (above). ([1](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L156-L168)) 26 28 6. Reduce the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop). ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php#L84-L125)) 27 29 -
optimization-detective/tags/1.0.0-beta2/docs/hooks.md
r3229869 r3240307 103 103 ``` 104 104 105 ### Filter: `od_url_metric_storage_lock_ttl` (default: 1 minute in seconds) 106 107 Filters how long a given IP is locked from submitting another metric-storage REST API request. Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: 105 ### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for authorized logged-in users) 106 107 Filters how long the current IP is locked from submitting another URL metric storage REST API request. 108 109 Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: 108 110 109 111 ```php … … 113 115 ``` 114 116 117 By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. 118 115 119 During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: 116 120 … … 121 125 ``` 122 126 123 ### Filter: `od_url_metric_freshness_ttl` (default: 1 dayin seconds)124 125 Filters the freshness age (TTL) for a given URL Metric. The freshness TTL must be at least zero, in which it considers URL Metrics to always be stale. In practice, the value should be at least an hour. If your site content does not change frequently, you may want to increase the TTL to a week:127 ### Filter: `od_url_metric_freshness_ttl` (default: 1 week in seconds) 128 129 Filters the freshness age (TTL) for a given URL Metric. The freshness TTL must be at least zero, in which it considers URL Metrics to always be stale. In practice, the value should be at least an hour. If your site content does not change frequently, you may want to increase the TTL even longer, say to a month: 126 130 127 131 ```php 128 132 add_filter( 'od_url_metric_freshness_ttl', static function (): int { 129 return WEEK_IN_SECONDS; 130 } ); 131 ``` 133 return MONTH_IN_SECONDS; 134 } ); 135 ``` 136 137 Note that even if you have large freshness TTL a URL Metric can still become stale sooner; if the page state changes then this results in a change to the ETag associated with a URL Metric. This will allow new URL Metrics to be collected before the freshness TTL has transpired. See the `od_current_url_metrics_etag_data` filter to customize the ETag data. 132 138 133 139 During development, this can be useful to set to zero so that you don't have to wait for new URL Metrics to be requested when engineering a new optimization: … … 230 236 See also [example usage](https://github.com/WordPress/performance/blob/6bb8405c5c446e3b66c2bfa3ae03ba61b188bca2/plugins/embed-optimizer/hooks.php#L128-L144) in Embed Optimizer. Note in particular the structure of the plugin’s [detect.js](https://github.com/WordPress/performance/blob/trunk/plugins/embed-optimizer/detect.js) script module, how it exports `initialize` and `finalize` functions which Optimization Detective then calls when the page loads and when the page unloads, at which time the URL Metric is constructed and sent to the server for storage. Refer also to the [TypeScript type definitions](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/types.ts). 231 237 232 ### Filter: `od_current_url_metrics_etag_data` (default: array with `tag_visitors` key)238 ### Filter: `od_current_url_metrics_etag_data` (default: `array<string, mixed>`) 233 239 234 240 Filters the data that goes into computing the current ETag for URL Metrics. 235 241 236 The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes the names of registered tag visitors. This ensures that when a new Optimization Detective-dependent plugin is activated (like [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) or [Embed Optimizer](https://wordpress.org/plugins/embed-optimizer/)), any existing URL Metrics are immediately considered stale. This happens because the newly registered tag visitors alter the ETag calculation, making it different from the stored ones. 237 238 When the ETag for URL Metrics in a complete viewport group no longer matches the current environment's ETag, new URL Metrics will then begin to be collected until there are no more stored URL Metrics with the old ETag. These new URL Metrics will include data relevant to the newly activated plugins and their tag visitors. 242 The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes: 243 244 1. The active theme and current version (for both parent and child themes). 245 2. The queried object ID, post type, and modified date. 246 3. The list of registered tag visitors. 247 4. The IDs and modified times of posts in The Loop. 248 5. The current theme template used to render the page. 249 6. The list of active plugins. 250 251 A change in ETag means that any previously-collected URL Metrics will be immediately considered stale. When the ETag for URL Metrics in a complete viewport group no longer matches the current environment's ETag, new URL Metrics will then begin to be collected until there are no more stored URL Metrics with the old ETag. -
optimization-detective/tags/1.0.0-beta2/helper.php
r3229869 r3240307 23 23 * Fires when extensions to Optimization Detective can be loaded and initialized. 24 24 * 25 * This action is useful for loading extension code that depends on Optimization Detective to be running. The version 26 * of the plugin is passed as the sole argument so that if the required version is not present, the callback can short circuit. 27 * 28 * Example: 29 * 30 * add_action( 'od_init', function ( string $version ) { 31 * if ( version_compare( $version, '1.0', '<' ) ) { 32 * add_action( 'admin_notices', 'my_plugin_warn_optimization_plugin_outdated' ); 33 * return; 34 * } 35 * 36 * // Bootstrap the Optimization Detective extension. 37 * require_once __DIR__ . '/functions.php'; 38 * // ... 39 * } ); 40 * 25 41 * @since 0.7.0 26 42 * … … 38 54 * @since 0.7.0 39 55 * 40 * @param int |null $minimum_viewport_width Minimum viewport width.41 * @param int |null $maximum_viewport_width Maximum viewport width.56 * @param int<0, max>|null $minimum_viewport_width Minimum viewport width (exclusive). 57 * @param int<1, max>|null $maximum_viewport_width Maximum viewport width (inclusive). 42 58 * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid. 43 59 */ 44 60 function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_viewport_width ): ?string { 45 if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width > $maximum_viewport_width ) {46 _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' );61 if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width >= $maximum_viewport_width ) { 62 _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than or equal to the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' ); 47 63 return null; 48 64 } 49 $media_attributes = array(); 50 if ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 ) { 51 $media_attributes[] = sprintf( '(min-width: %dpx)', $minimum_viewport_width ); 52 } 53 if ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ) { 54 $media_attributes[] = sprintf( '(max-width: %dpx)', $maximum_viewport_width ); 55 } 56 if ( count( $media_attributes ) === 0 ) { 65 $has_min_width = ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 ); 66 $has_max_width = ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ); // Note: The use of PHP_INT_MAX is obsolete. 67 if ( $has_min_width && $has_max_width ) { 68 return sprintf( '(%dpx < width <= %dpx)', $minimum_viewport_width, $maximum_viewport_width ); 69 } elseif ( $has_min_width ) { 70 return sprintf( '(%dpx < width)', $minimum_viewport_width ); 71 } elseif ( $has_max_width ) { 72 return sprintf( '(width <= %dpx)', $maximum_viewport_width ); 73 } else { 57 74 return null; 58 75 } 59 return join( ' and ', $media_attributes );60 76 } 61 77 -
optimization-detective/tags/1.0.0-beta2/hooks.php
r3229869 r3240307 19 19 add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX ); 20 20 OD_URL_Metrics_Post_Type::add_hooks(); 21 OD_Storage_Lock::add_hooks(); 21 22 add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' ); 22 23 add_action( 'wp_head', 'od_render_generator_meta_tag' ); -
optimization-detective/tags/1.0.0-beta2/load.php
r3229869 r3240307 3 3 * Plugin Name: Optimization Detective 4 4 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective 5 * Description: Provides a n API for leveraging real user metrics to detect optimizations to apply on the frontend to improvepage performance.5 * Description: Provides a framework for leveraging real user metrics to detect optimizations for improving page performance. 6 6 * Requires at least: 6.6 7 7 * Requires PHP: 7.2 8 * Version: 1.0.0-beta 18 * Version: 1.0.0-beta2 9 9 * Author: WordPress Performance Team 10 10 * Author URI: https://make.wordpress.org/performance/ … … 72 72 )( 73 73 'optimization_detective_pending_plugin', 74 '1.0.0-beta 1',74 '1.0.0-beta2', 75 75 static function ( string $version ): void { 76 76 if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { -
optimization-detective/tags/1.0.0-beta2/optimization.php
r3229869 r3240307 168 168 // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway). 169 169 is_embed() || 170 // Skip posts that aren't published yet. 171 is_preview() || 170 172 // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context. 171 173 is_customize_preview() || … … 252 254 * Fires to register tag visitors before walking over the document to perform optimizations. 253 255 * 256 * Once a page has finished rendering and the output buffer is processed, the page contents are loaded into 257 * an HTML Tag Processor instance. It then iterates over each tag in the document, and at each open tag it will 258 * invoke all registered tag visitors. A tag visitor is simply a callable (such as a regular function, closure, 259 * or even a class with an `__invoke` method defined). The tag visitor callback is invoked by passing an instance 260 * of the `OD_Tag_Visitor_Context` object which includes the following read-only properties: 261 * 262 * - `$processor` (`OD_HTML_Tag_Processor`): The processor with the cursor at the current open tag. 263 * - `$url_metric_group_collection` (`OD_URL_Metric_Group_Collection`): The URL Metrics which may include information about the current tag to inform what optimizations the callback performs. 264 * - `$link_collection` (`OD_Link_Collection`): Collection of links which will be added to the `HEAD` when the page is served. This allows you to add preload links and preconnect links as needed. 265 * - `$url_metrics_id` (`positive-int|null`): The post ID for the `od_url_metrics` post from which the URL Metrics were loaded (if any). For advanced usage. 266 * 267 * Note that you are free to call `$processor->next_tag()` in the callback (such as to walk over any child elements) 268 * since the tag processor's cursor will be reset to the tag after the callback finishes. 269 * 270 * When a tag visitor sees it is at a relevant open tag (e.g. by checking `$processor->get_tag()`), it can call the 271 * `$context->track_tag()` method to indicate that the tag should be measured during detection. This will cause the 272 * tag to be included among the `elements` in the stored URL Metrics. The element data includes properties such 273 * as `intersectionRatio`, `intersectionRect`, and `boundingClientRect` (provided by an `IntersectionObserver`) as 274 * well as whether the tag is the LCP element (`isLCP`) or LCP element candidate (`isLCPCandidate`). This method 275 * should not be called if the current tag is not relevant for the tag visitor or if the tag visitor callback does 276 * not need to query the provided `OD_URL_Metric_Group_Collection` instance to apply the desired optimizations. (In 277 * addition to calling the `$context->track_tag()`, a callback may also return `true` to indicate the tag should be 278 * tracked.) 279 * 280 * Here's an example tag visitor that depends on URL Metrics data: 281 * 282 * $tag_visitor_registry->register( 283 * 'lcp-img-fetchpriority-high', 284 * static function ( OD_Tag_Visitor_Context $context ): void { 285 * if ( $context->processor->get_tag() !== 'IMG' ) { 286 * return; // Tag is not relevant for this tag visitor. 287 * } 288 * 289 * // Mark the tag for measurement during detection so it is included among the elements stored in URL Metrics. 290 * $context->track_tag(); 291 * 292 * // Make sure fetchpriority=high is added to LCP IMG elements based on the captured URL Metrics. 293 * $common_lcp_element = $context->url_metric_group_collection->get_common_lcp_element(); 294 * if ( 295 * null !== $common_lcp_element 296 * && 297 * $common_lcp_element->get_xpath() === $context->processor->get_xpath() 298 * ) { 299 * $context->processor->set_attribute( 'fetchpriority', 'high' ); 300 * } 301 * } 302 * ); 303 * 304 * Please note this implementation of setting `fetchpriority=high` on the LCP `IMG` element is simplified. Please 305 * see the Image Prioritizer extension for a more robust implementation. 306 * 307 * Here's an example tag visitor that does not depend on any URL Metrics data: 308 * 309 * $tag_visitor_registry->register( 310 * 'img-decoding-async', 311 * static function ( OD_Tag_Visitor_Context $context ): bool { 312 * if ( $context->processor->get_tag() !== 'IMG' ) { 313 * return; // Tag is not relevant for this tag visitor. 314 * } 315 * 316 * // Set the decoding attribute if it is absent. 317 * if ( null === $context->processor->get_attribute( 'decoding' ) ) { 318 * $context->processor->set_attribute( 'decoding', 'async' ); 319 * } 320 * } 321 * ); 322 * 323 * Refer to [Image Prioritizer](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer) and 324 * [Embed Optimizer](https://github.com/WordPress/performance/tree/trunk/plugins/embed-optimizer) for additional 325 * examples of how tag visitors are used. 326 * 254 327 * @since 0.3.0 255 328 * … … 268 341 $link_collection = new OD_Link_Collection(); 269 342 $visited_tag_state = new OD_Visited_Tag_State(); 270 $tag_visitor_context = new OD_Tag_Visitor_Context( $processor, $group_collection, $link_collection, $visited_tag_state ); 343 $tag_visitor_context = new OD_Tag_Visitor_Context( 344 $processor, 345 $group_collection, 346 $link_collection, 347 $visited_tag_state, 348 $post instanceof WP_Post && $post->ID > 0 ? $post->ID : null 349 ); 271 350 $current_tag_bookmark = 'optimization_detective_current_tag'; 272 351 $visitors = iterator_to_array( $tag_visitor_registry ); -
optimization-detective/tags/1.0.0-beta2/readme.txt
r3229869 r3240307 3 3 Contributors: wordpressdotorg 4 4 Tested up to: 6.7 5 Stable tag: 1.0.0-beta 15 Stable tag: 1.0.0-beta2 6 6 License: GPLv2 or later 7 7 License URI: https://www.gnu.org/licenses/gpl-2.0.html 8 8 Tags: performance, optimization, rum 9 9 10 Provides a n API for leveraging real user metrics to detect optimizations to apply on the frontend to improvepage performance.10 Provides a framework for leveraging real user metrics to detect optimizations for improving page performance. 11 11 12 12 == Description == … … 55 55 56 56 == Changelog == 57 58 = 1.0.0-beta2 = 59 60 **Enhancements** 61 62 * Account for 64 KiB limit for sending beacon data. ([1851](https://github.com/WordPress/performance/pull/1851)) 63 * Add post ID for the `od_url_metrics` post to the tag visitor context. ([1847](https://github.com/WordPress/performance/pull/1847)) 64 * Change minimum viewport width to be exclusive whereas the maximum width remains inclusive. ([1839](https://github.com/WordPress/performance/pull/1839)) 65 * Disable URL Metric storage locking by default for administrators. ([1835](https://github.com/WordPress/performance/pull/1835)) 66 * Include active plugins in ETag data and increase default freshness TTL from 1 day to 1 week. ([1854](https://github.com/WordPress/performance/pull/1854)) 67 * Make ETag a required property of the URL Metric. ([1824](https://github.com/WordPress/performance/pull/1824)) 68 * Use CSS range syntax in media queries. ([1833](https://github.com/WordPress/performance/pull/1833)) 69 * Use `IFRAME` to display HTML responses for REST API storage request failures in Site Health test. ([1849](https://github.com/WordPress/performance/pull/1849)) 70 71 **Bug Fixes** 72 73 * Prevent URL in `Link` header from including invalid characters. ([1802](https://github.com/WordPress/performance/pull/1802)) 74 * Prevent optimizing post previews by default. ([1848](https://github.com/WordPress/performance/pull/1848)) 75 76 **Documentation** 77 78 * Improve Optimization Detective documentation. ([1782](https://github.com/WordPress/performance/pull/1782)) 57 79 58 80 = 1.0.0-beta1 = -
optimization-detective/tags/1.0.0-beta2/site-health.php
r3229869 r3240307 128 128 $body = wp_remote_retrieve_body( $response ); 129 129 $data = json_decode( $body, true ); 130 $header = wp_remote_retrieve_header( $response, 'content-type' ); 131 if ( is_array( $header ) ) { 132 $header = array_pop( $header ); 133 } 130 134 131 135 $is_expected = ( … … 157 161 } 158 162 159 $result['description'] .= '<details><summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary><pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre></details>'; 163 if ( '' !== $body ) { 164 $result['description'] .= '<details>'; 165 $result['description'] .= '<summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary>'; 166 167 if ( is_string( $header ) && str_contains( $header, 'html' ) ) { 168 $escaped_content = htmlspecialchars( $body, ENT_QUOTES, 'UTF-8' ); 169 $result['description'] .= '<iframe srcdoc="' . $escaped_content . '" sandbox width="100%" height="300"></iframe>'; 170 } else { 171 $result['description'] .= '<pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre>'; 172 } 173 $result['description'] .= '</details>'; 174 } 160 175 } 161 176 } … … 239 254 } 240 255 241 wp_admin_notice(256 $notice = wp_get_admin_notice( 242 257 $message, 243 258 array( … … 247 262 ) 248 263 ); 264 265 echo wp_kses( 266 $notice, 267 array_merge( 268 wp_kses_allowed_html( 'post' ), 269 array( 270 'iframe' => array_fill_keys( array( 'srcdoc', 'sandbox', 'width', 'height' ), true ), 271 ) 272 ) 273 ); 249 274 } 250 275 … … 255 280 * @access private 256 281 * 257 * @param string $plugin_file Plugin file.282 * @param non-empty-string $plugin_file Plugin file. 258 283 */ 259 284 function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void { -
optimization-detective/tags/1.0.0-beta2/storage/class-od-storage-lock.php
r3229869 r3240307 22 22 23 23 /** 24 * Capability for being able to store a URL Metric now. 25 * 26 * @since 1.0.0 27 * @var string 28 */ 29 const STORE_URL_METRIC_NOW_CAPABILITY = 'od_store_url_metric_now'; 30 31 /** 32 * Adds hooks. 33 * 34 * @since 1.0.0 35 */ 36 public static function add_hooks(): void { 37 add_filter( 'user_has_cap', array( __CLASS__, 'filter_user_has_cap' ) ); 38 } 39 40 /** 41 * Filters `user_has_cap` to grant the `od_store_url_metric_now` capability to users who can `manage_options` by default. 42 * 43 * @since 1.0.0 44 * 45 * @param array<string, bool>|mixed $allcaps Capability names mapped to boolean values for whether the user has that capability. 46 * @return array<string, bool> Capability names mapped to boolean values for whether the user has that capability. 47 */ 48 public static function filter_user_has_cap( $allcaps ): array { 49 if ( ! is_array( $allcaps ) ) { 50 $allcaps = array(); 51 } 52 if ( isset( $allcaps['manage_options'] ) ) { 53 $allcaps['od_store_url_metric_now'] = $allcaps['manage_options']; 54 } 55 return $allcaps; 56 } 57 58 /** 24 59 * Gets the TTL (in seconds) for the URL Metric storage lock. 25 60 * 26 61 * @since 0.1.0 27 * @access private28 62 * 29 * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used.63 * @return int<0, max> TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. 30 64 */ 31 65 public static function get_ttl(): int { 66 $ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS; 32 67 33 68 /** 34 * Filters how long a given IP is locked from submitting another metric-storage REST API request.69 * Filters how long the current IP is locked from submitting another URL metric storage REST API request. 35 70 * 36 * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable71 * Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable 37 72 * locking when a user is logged-in with code like the following: 38 73 * … … 41 76 * } ); 42 77 * 78 * By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current 79 * user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This 80 * custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. 81 * 43 82 * @since 0.1.0 83 * @since 1.0.0 This now defaults to zero (0) for authorized users. 44 84 * 45 * @param int $ttl TTL. 85 * @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users. 46 86 */ 47 $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS);87 $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', $ttl ); 48 88 return max( 0, $ttl ); 49 89 } … … 52 92 * Gets transient key for locking URL Metric storage (for the current IP). 53 93 * 54 * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? 55 * @return string Transient key. 94 * @since 0.1.0 95 * 96 * @return non-empty-string Transient key. 56 97 */ 57 98 public static function get_transient_key(): string { … … 67 108 * 68 109 * @since 0.1.0 69 * @access private70 110 */ 71 111 public static function set_lock(): void { … … 83 123 * 84 124 * @since 0.1.0 85 * @access private86 125 * 87 126 * @return bool Whether locked. -
optimization-detective/tags/1.0.0-beta2/storage/class-od-url-metric-store-request-context.php
r3229869 r3240307 17 17 * 18 18 * @since 0.7.0 19 * @access private 19 * 20 * @property-read WP_REST_Request<array<string, mixed>> $request Request. 21 * @property-read positive-int $url_metrics_id ID for the od_url_metrics post. 22 * @property-read OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 23 * @property-read OD_URL_Metric_Group $url_metric_group URL Metric group. 24 * @property-read OD_URL_Metric $url_metric URL Metric. 25 * @property-read positive-int $post_id Deprecated alias for the $url_metrics_id property. 20 26 */ 21 27 final class OD_URL_Metric_Store_Request_Context { … … 24 30 * Request. 25 31 * 32 * @since 0.7.0 26 33 * @var WP_REST_Request<array<string, mixed>> 27 * @readonly28 34 */ 29 p ublic$request;35 private $request; 30 36 31 37 /** 32 * ID for the URL Metricpost.38 * ID for the od_url_metrics post. 33 39 * 34 * @var int 35 * @readonly 40 * This was originally $post_id which was introduced in 0.7.0. 41 * 42 * @since 1.0.0 43 * @var positive-int 36 44 */ 37 p ublic $post_id;45 private $url_metrics_id; 38 46 39 47 /** 40 48 * URL Metric group collection. 41 49 * 50 * @since 0.7.0 42 51 * @var OD_URL_Metric_Group_Collection 43 * @readonly44 52 */ 45 p ublic$url_metric_group_collection;53 private $url_metric_group_collection; 46 54 47 55 /** 48 56 * URL Metric group. 49 57 * 58 * @since 0.7.0 50 59 * @var OD_URL_Metric_Group 51 * @readonly52 60 */ 53 p ublic$url_metric_group;61 private $url_metric_group; 54 62 55 63 /** 56 64 * URL Metric. 57 65 * 66 * @since 0.7.0 58 67 * @var OD_URL_Metric 59 * @readonly60 68 */ 61 p ublic$url_metric;69 private $url_metric; 62 70 63 71 /** 64 72 * Constructor. 65 73 * 74 * @since 0.7.0 75 * 66 76 * @phpstan-param WP_REST_Request<array<string, mixed>> $request 67 77 * 68 78 * @param WP_REST_Request $request REST API request. 69 * @param int $post_idID for the URL Metric post.79 * @param positive-int $url_metrics_id ID for the URL Metric post. 70 80 * @param OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 71 81 * @param OD_URL_Metric_Group $url_metric_group URL Metric group. 72 82 * @param OD_URL_Metric $url_metric URL Metric. 73 83 */ 74 public function __construct( WP_REST_Request $request, int $ post_id, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_URL_Metric_Group $url_metric_group, OD_URL_Metric $url_metric ) {84 public function __construct( WP_REST_Request $request, int $url_metrics_id, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_URL_Metric_Group $url_metric_group, OD_URL_Metric $url_metric ) { 75 85 $this->request = $request; 76 $this-> post_id = $post_id;86 $this->url_metrics_id = $url_metrics_id; 77 87 $this->url_metric_group_collection = $url_metric_group_collection; 78 88 $this->url_metric_group = $url_metric_group; 79 89 $this->url_metric = $url_metric; 80 90 } 91 92 /** 93 * Gets a property. 94 * 95 * @since 1.0.0 96 * 97 * @param string $name Property name. 98 * @return mixed Property value. 99 * 100 * @throws Error When property is unknown. 101 */ 102 public function __get( string $name ) { 103 switch ( $name ) { 104 case 'request': 105 return $this->request; 106 case 'url_metrics_id': 107 return $this->url_metrics_id; 108 case 'url_metric_group_collection': 109 return $this->url_metric_group_collection; 110 case 'url_metric_group': 111 return $this->url_metric_group; 112 case 'url_metric': 113 return $this->url_metric; 114 case 'post_id': 115 _doing_it_wrong( 116 esc_html( __CLASS__ . '::$' . $name ), 117 esc_html( 118 sprintf( 119 /* translators: %s is class member variable name */ 120 __( 'Use %s instead.', 'optimization-detective' ), 121 __CLASS__ . '::$url_metrics_id' 122 ) 123 ), 124 'optimization-detective 1.0.0' 125 ); 126 return $this->url_metrics_id; 127 default: 128 throw new Error( 129 esc_html( 130 sprintf( 131 /* translators: %s is class member variable name */ 132 __( 'Unknown property %s.', 'optimization-detective' ), 133 __CLASS__ . '::$' . $name 134 ) 135 ) 136 ); 137 } 138 } 81 139 } -
optimization-detective/tags/1.0.0-beta2/storage/class-od-url-metrics-post-type.php
r3229869 r3240307 24 24 * Post type slug. 25 25 * 26 * @since 0.1.0 26 27 * @var string 27 28 */ … … 31 32 * Event name (hook) for garbage collection of stale URL Metrics posts. 32 33 * 34 * @since 0.1.0 33 35 * @var string 34 36 */ … … 38 40 * Recurrence for garbage collection of stale URL Metrics posts. 39 41 * 42 * @since 0.1.0 40 43 * @var string 41 44 */ … … 85 88 * @since 0.1.0 86 89 * 87 * @param string $slug URL Metrics slug.90 * @param non-empty-string $slug URL Metrics slug. 88 91 * @return WP_Post|null Post object if exists. 89 92 */ … … 203 206 * @todo There is duplicate logic here with od_handle_rest_request(). 204 207 * 205 * @param string $slugSlug (hash of normalized query vars).206 * @param OD_URL_Metric $new_url_metric New URL Metric.207 * @return int|WP_Error Post ID or WP_Error otherwise.208 * @param non-empty-string $slug Slug (hash of normalized query vars). 209 * @param OD_URL_Metric $new_url_metric New URL Metric. 210 * @return positive-int|WP_Error Post ID or WP_Error otherwise. 208 211 */ 209 212 public static function store_url_metric( string $slug, OD_URL_Metric $new_url_metric ) { … … 227 230 228 231 $etag = $new_url_metric->get_etag(); 229 if ( null === $etag ) {230 // This case actually will never occur in practice because the store_url_metric function is only called231 // in the REST API endpoint where the ETag parameter is required. It is here exclusively for the sake of232 // PHPStan's static analysis. This entire condition can be removed in a future release when the 'etag'233 // property becomes required.234 return new WP_Error( 'missing_etag' );235 }236 232 237 233 $group_collection = new OD_URL_Metric_Group_Collection( … … 251 247 252 248 $post_data['post_content'] = wp_json_encode( 253 array_map( 254 static function ( OD_URL_Metric $url_metric ): array { 255 return $url_metric->jsonSerialize(); 256 }, 257 $group_collection->get_flattened_url_metrics() 258 ), 259 JSON_UNESCAPED_SLASHES // No need for escaped slashes since not printed to frontend. 249 $group_collection->get_flattened_url_metrics(), 250 JSON_UNESCAPED_SLASHES // No need for escaping slashes since this JSON is not embedded in HTML. 260 251 ); 261 252 if ( ! is_string( $post_data['post_content'] ) ) { -
optimization-detective/tags/1.0.0-beta2/storage/data.php
r3229869 r3240307 21 21 * @access private 22 22 * 23 * @return int Expiration TTL in seconds.23 * @return int<0, max> Expiration TTL in seconds. 24 24 */ 25 25 function od_get_url_metric_freshness_ttl(): int { … … 32 32 * @since 0.1.0 33 33 * 34 * @param int $ttl Expiration TTL in seconds. Defaults to 1 day.35 */ 36 $freshness_ttl = (int) apply_filters( 'od_url_metric_freshness_ttl', DAY_IN_SECONDS );34 * @param int $ttl Expiration TTL in seconds. Defaults to 1 week. 35 */ 36 $freshness_ttl = (int) apply_filters( 'od_url_metric_freshness_ttl', WEEK_IN_SECONDS ); 37 37 38 38 if ( $freshness_ttl < 0 ) { … … 59 59 * This is used as a cache key for stored URL Metrics. 60 60 * 61 * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache.62 *63 61 * @since 0.1.0 64 62 * @access private … … 118 116 $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ); 119 117 } 118 119 // TODO: We should be able to assert that this returns an non-empty-string. 120 120 return esc_url_raw( $current_url ); 121 121 } … … 132 132 * 133 133 * @param array<string, mixed> $query_vars Normalized query vars. 134 * @return string Slug.134 * @return non-empty-string Slug. 135 135 */ 136 136 function od_get_url_metrics_slug( array $query_vars ): string { … … 199 199 $queried_object_data['type'] = $queried_object->name; 200 200 } 201 202 $active_plugins = (array) get_option( 'active_plugins', array() ); 203 if ( is_multisite() ) { 204 $active_plugins = array_unique( 205 array_merge( 206 $active_plugins, 207 array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) 208 ) 209 ); 210 } 211 sort( $active_plugins ); 201 212 202 213 $data = array( … … 231 242 ), 232 243 ), 244 'active_plugins' => $active_plugins, 233 245 'current_template' => $current_template instanceof WP_Block_Template ? get_object_vars( $current_template ) : $current_template, 234 246 ); … … 258 270 * @see od_get_url_metrics_slug() 259 271 * 260 * @param string$slug Slug (hash of normalized query vars).261 * @param non-empty-string $current_etag Current ETag.262 * @param string $url URL.263 * @param int|null$cache_purge_post_id Cache purge post ID.264 * @return string HMAC.272 * @param non-empty-string $slug Slug (hash of normalized query vars). 273 * @param non-empty-string $current_etag Current ETag. 274 * @param string $url URL. 275 * @param positive-int|null $cache_purge_post_id Cache purge post ID. 276 * @return non-empty-string HMAC. 265 277 */ 266 278 function od_get_url_metrics_storage_hmac( string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): string { 267 279 $action = "store_url_metric:$slug:$current_etag:$url:$cache_purge_post_id"; 268 return wp_hash( $action, 'nonce' ); 280 281 /** 282 * HMAC. 283 * 284 * @var non-empty-string $hmac 285 */ 286 $hmac = wp_hash( $action, 'nonce' ); 287 return $hmac; 269 288 } 270 289 … … 279 298 * @see od_get_url_metrics_slug() 280 299 * 281 * @param string$hmac HMAC.282 * @param string$slug Slug (hash of normalized query vars).283 * @param non-empty-string $current_etag Current ETag.284 * @param string $url URL.285 * @param int|null$cache_purge_post_id Cache purge post ID.300 * @param non-empty-string $hmac HMAC. 301 * @param non-empty-string $slug Slug (hash of normalized query vars). 302 * @param non-empty-string $current_etag Current ETag. 303 * @param string $url URL. 304 * @param positive-int|null $cache_purge_post_id Cache purge post ID. 286 305 * @return bool Whether the HMAC is valid. 287 306 */ … … 361 380 * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13 362 381 * 363 * @return int[] Breakpoint max widths, sorted in ascending order.382 * @return positive-int[] Breakpoint max widths, sorted in ascending order. 364 383 */ 365 384 function od_get_breakpoint_max_widths(): array { … … 369 388 static function ( $original_breakpoint ) use ( $function_name ): int { 370 389 $breakpoint = $original_breakpoint; 371 if ( PHP_INT_MAX === $breakpoint ) { 372 $breakpoint = PHP_INT_MAX - 1; 373 _doing_it_wrong( 374 esc_html( $function_name ), 375 esc_html( 376 sprintf( 377 /* translators: %s is the actual breakpoint max width */ 378 __( 'Breakpoint must be less than PHP_INT_MAX, but saw "%s".', 'optimization-detective' ), 379 $original_breakpoint 380 ) 381 ), 382 '' 383 ); 384 } elseif ( $breakpoint <= 0 ) { 390 if ( $breakpoint <= 0 ) { 385 391 $breakpoint = 1; 386 392 _doing_it_wrong( … … 401 407 * Filters the breakpoint max widths to group URL Metrics for various viewports. 402 408 * 403 * A breakpoint must be greater than zero and less than PHP_INT_MAX. This array may be empty in which case there409 * A breakpoint must be greater than zero. This array may be empty in which case there 404 410 * are no responsive breakpoints and all URL Metrics are collected in a single group. 405 411 * 406 412 * @since 0.1.0 407 413 * 408 * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. Defaults to [480, 600, 782].414 * @param positive-int[] $breakpoint_max_widths Max widths for viewport breakpoints. Defaults to [480, 600, 782]. 409 415 */ 410 416 array_map( 'intval', (array) apply_filters( 'od_breakpoint_max_widths', array( 480, 600, 782 ) ) ) … … 426 432 * @access private 427 433 * 428 * @return int Sample size.434 * @return int<1, max> Sample size. 429 435 */ 430 436 function od_get_url_metrics_breakpoint_sample_size(): int { -
optimization-detective/tags/1.0.0-beta2/storage/rest-api.php
r3229869 r3240307 16 16 * Namespace for optimization-detective. 17 17 * 18 * @since 0.1.0 19 * @access private 18 20 * @var string 19 21 */ … … 27 29 * create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug. 28 30 * 31 * @since 0.1.0 32 * @access private 29 33 * @link https://google.aip.dev/136 30 34 * @var string … … 73 77 'pattern' => '^[0-9a-f]+\z', 74 78 'validate_callback' => static function ( string $hmac, WP_REST_Request $request ) { 75 if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['current_etag'], $request['url'], $request['cache_purge_post_id'] ?? null ) ) {79 if ( '' === $hmac || ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['current_etag'], $request['url'], $request['cache_purge_post_id'] ?? null ) ) { 76 80 return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) ); 77 81 } … … 215 219 } 216 220 221 /* 222 * The limit for data sent via navigator.sendBeacon() is 64 KiB. This limit is checked in detect.js so that the 223 * request will not even be attempted if the payload is too large. This server-side restriction is added as a 224 * safeguard against clients sending possibly malicious payloads much larger than 64 KiB which should never be 225 * getting sent. 226 */ 227 $max_size = 64 * 1024; 228 $content_length = strlen( (string) wp_json_encode( $url_metric ) ); 229 if ( $content_length > $max_size ) { 230 return new WP_Error( 231 'rest_content_too_large', 232 sprintf( 233 /* translators: 1: the size of the payload, 2: the maximum allowed payload size */ 234 __( 'JSON payload size is %1$s bytes which is larger than the maximum allowed size of %2$s bytes.', 'optimization-detective' ), 235 number_format_i18n( $content_length ), 236 number_format_i18n( $max_size ) 237 ), 238 array( 'status' => 413 ) 239 ); 240 } 241 217 242 // TODO: This should be changed from store_url_metric($slug, $url_metric) instead be update_post( $slug, $group_collection ). As it stands, store_url_metric() is duplicating logic here. 218 243 $result = OD_URL_Metrics_Post_Type::store_url_metric( -
optimization-detective/tags/1.0.0-beta2/types.ts
r3229869 r3240307 35 35 export interface URLMetricGroupStatus { 36 36 minimumViewportWidth: number; 37 maximumViewportWidth: number | null; 37 38 complete: boolean; 38 39 } -
optimization-detective/trunk/class-od-data-validation-exception.php
r3229869 r3240307 17 17 * 18 18 * @since 0.1.0 19 * @access private20 19 */ 21 20 final class OD_Data_Validation_Exception extends Exception {} -
optimization-detective/trunk/class-od-element.php
r3229869 r3240307 22 22 * 23 23 * @since 0.7.0 24 * @access private25 24 */ 26 25 class OD_Element implements ArrayAccess, JsonSerializable { -
optimization-detective/trunk/class-od-html-tag-processor.php
r3229869 r3240307 17 17 * 18 18 * @since 0.1.1 19 * @access private20 19 */ 21 20 final class OD_HTML_Tag_Processor extends WP_HTML_Tag_Processor { … … 150 149 * 151 150 * @since 0.4.0 152 * @var string[]151 * @var non-empty-string[] 153 152 */ 154 153 private $open_stack_tags = array(); … … 161 160 * 162 161 * @since 1.0.0 163 * @var array<array< string, string>>162 * @var array<array<non-empty-string, string>> 164 163 */ 165 164 private $open_stack_attributes = array(); … … 169 168 * 170 169 * @since 0.4.0 171 * @var int[]170 * @var non-negative-int[] 172 171 */ 173 172 private $open_stack_indices = array(); … … 182 181 * 183 182 * @since 0.4.0 184 * @var array<string, array{tags: string[], attributes: array<array<string, string>>, indices:int[]}>183 * @var array<string, array{tags: non-empty-string[], attributes: array<array<non-empty-string, string>>, indices: non-negative-int[]}> 185 184 */ 186 185 private $bookmarked_open_stacks = array(); … … 222 221 * 223 222 * @since 0.4.0 224 * @var array< string, string[]>223 * @var array<non-empty-string, string[]> 225 224 */ 226 225 private $buffered_text_replacements = array(); … … 239 238 * 240 239 * @since 0.6.0 241 * @var int240 * @var non-negative-int 242 241 * @see self::next_token() 243 242 * @see self::seek() … … 293 292 * @since 0.4.0 294 293 * 295 * @param string|null $tag_name Tag name, if not provided then the current tag is used. Optional.294 * @param non-empty-string|null $tag_name Tag name, if not provided then the current tag is used. Optional. 296 295 * @return bool Whether to expect a closer for the tag. 297 296 */ … … 337 336 return true; 338 337 } 338 /** 339 * Tag name. 340 * 341 * @var non-empty-string $tag_name 342 */ 339 343 340 344 if ( $this->previous_tag_without_closer ) { … … 423 427 * @see self::seek() 424 428 * 425 * @return int Count of times the cursor has moved.429 * @return non-negative-int Count of times the cursor has moved. 426 430 */ 427 431 public function get_cursor_move_count(): int { … … 459 463 * @since 0.4.0 460 464 * 461 * @param string$name Meta attribute name.462 * @param string|true $value Value.465 * @param non-empty-string $name Meta attribute name. 466 * @param string|true $value Value. 463 467 * @return bool Whether an attribute was set. 464 468 */ … … 490 494 * @see WP_HTML_Processor::get_current_depth() 491 495 * 492 * @return int Nesting-depth of current location in the document.496 * @return non-negative-int Nesting-depth of current location in the document. 493 497 */ 494 498 public function get_current_depth(): int { … … 566 570 * @since 0.9.0 Renamed from get_breadcrumbs() to get_indexed_breadcrumbs(). 567 571 * 568 * @return Generator<array{ string, int, array<string, string>}> Breadcrumb.572 * @return Generator<array{non-empty-string, non-negative-int, array<non-empty-string, string>}> Breadcrumb. 569 573 */ 570 574 private function get_indexed_breadcrumbs(): Generator { … … 585 589 * @since 1.0.0 586 590 * 587 * @return array< string, string> Disambiguating attributes.591 * @return array<non-empty-string, string> Disambiguating attributes. 588 592 */ 589 593 private function get_disambiguating_attributes(): array { … … 618 622 * @see WP_HTML_Processor::get_breadcrumbs() 619 623 * 620 * @return string[] Array of tag names representing path to matched node.624 * @return non-empty-string[] Array of tag names representing path to matched node. 621 625 */ 622 626 public function get_breadcrumbs(): array { -
optimization-detective/trunk/class-od-link-collection.php
r3229869 r3240307 19 19 * attributes: LinkAttributes, 20 20 * minimum_viewport_width: int<0, max>|null, 21 * maximum_viewport_width: positive-int|null21 * maximum_viewport_width: int<1, max>|null 22 22 * } 23 23 * … … 38 38 * @since 0.3.0 39 39 * @since 0.4.0 Renamed from OD_Preload_Link_Collection. 40 * @access private41 40 */ 42 41 final class OD_Link_Collection implements Countable { … … 45 44 * Links grouped by rel type. 46 45 * 46 * @since 0.4.0 47 * 47 48 * @var array<string, Link[]> 48 49 */ … … 52 53 * Adds link. 53 54 * 55 * @since 0.3.0 56 * 54 57 * @phpstan-param LinkAttributes $attributes 55 58 * 56 * @param array $attributes Attributes.57 * @param int<0, max>|null $minimum_viewport_width Minimum widthor null if not bounded or relevant.58 * @param positive-int|null $maximum_viewport_width Maximum widthor null if not bounded (i.e. infinity) or relevant.59 * @param array $attributes Attributes. 60 * @param int<0, max>|null $minimum_viewport_width Minimum width (exclusive) or null if not bounded or relevant. 61 * @param int<1, max>|null $maximum_viewport_width Maximum width (inclusive) or null if not bounded (i.e. infinity) or relevant. 59 62 * 60 63 * @throws InvalidArgumentException When invalid arguments are provided. … … 112 115 * together. Also, add media attributes to the links. 113 116 * 117 * @since 0.4.0 118 * 114 119 * @return LinkAttributes[] Prepared links with adjacent-duplicates merged together and media attributes added. 115 120 */ … … 133 138 /** 134 139 * Merges consecutive links. 140 * 141 * @since 0.4.0 135 142 * 136 143 * @param Link[] $links Links. … … 195 202 is_int( $last_link['maximum_viewport_width'] ) 196 203 && 197 $last_link['maximum_viewport_width'] + 1=== $link['minimum_viewport_width']204 $last_link['maximum_viewport_width'] === $link['minimum_viewport_width'] 198 205 ) { 199 $last_link['maximum_viewport_width'] = max( $last_link['maximum_viewport_width'], $link['maximum_viewport_width'] );206 $last_link['maximum_viewport_width'] = null === $link['maximum_viewport_width'] ? null : max( $last_link['maximum_viewport_width'], $link['maximum_viewport_width'] ); 200 207 201 208 // Update the last link with the new maximum viewport width. … … 229 236 * Gets the HTML for the link tags. 230 237 * 238 * @since 0.3.0 239 * 231 240 * @return string Link tags HTML. 232 241 */ … … 250 259 * Constructs the Link HTTP response header. 251 260 * 252 * @return string|null Link HTTP response header, or null if there are none. 261 * @since 0.4.0 262 * 263 * @return non-empty-string|null Link HTTP response header, or null if there are none. 253 264 */ 254 265 public function get_response_header(): ?string { … … 256 267 257 268 foreach ( $this->get_prepared_links() as $link ) { 258 // The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded. 259 $link['href'] = isset( $link['href'] ) ? esc_url_raw( $link['href'] ) : 'about:blank'; 260 $link_header = '<' . $link['href'] . '>'; 269 if ( isset( $link['href'] ) ) { 270 $link['href'] = $this->encode_url_for_response_header( $link['href'] ); 271 } else { 272 // The about:blank is present since a Link without a reference-uri is invalid so any imagesrcset would otherwise not get downloaded. 273 $link['href'] = 'about:blank'; 274 } 275 276 // Encode the URLs in the srcset. 277 if ( isset( $link['imagesrcset'] ) ) { 278 $link['imagesrcset'] = join( 279 ', ', 280 array_map( 281 function ( $image_candidate ) { 282 // Parse out the URL to separate it from the descriptor. 283 $image_candidate_parts = (array) preg_split( '/\s+/', (string) $image_candidate, 2 ); 284 285 // Encode the URL. 286 $image_candidate_parts[0] = $this->encode_url_for_response_header( (string) $image_candidate_parts[0] ); 287 288 // Re-join the URL with the descriptor. 289 return implode( ' ', $image_candidate_parts ); 290 }, 291 (array) preg_split( '/\s*,\s*/', $link['imagesrcset'] ) 292 ) 293 ); 294 } 295 296 $link_header = '<' . $link['href'] . '>'; 261 297 unset( $link['href'] ); 262 298 foreach ( $link as $name => $value ) { … … 286 322 287 323 /** 324 * Encodes a URL for serving in an HTTP response header. 325 * 326 * @since n.e.x.t 327 * 328 * @param string $url URL to percent encode. Any existing percent encodings will first be decoded. 329 * @return string Percent-encoded URL. 330 */ 331 private function encode_url_for_response_header( string $url ): string { 332 $decoded_url = urldecode( $url ); 333 334 // Encode characters not allowed in a URL per RFC 3986 (anything that is not among the reserved and unreserved characters). 335 $encoded_url = (string) preg_replace_callback( 336 '/[^A-Za-z0-9\-._~:\/?#\[\]@!$&\'()*+,;=]/', 337 static function ( $matches ) { 338 return rawurlencode( $matches[0] ); 339 }, 340 $decoded_url 341 ); 342 return esc_url_raw( $encoded_url ); 343 } 344 345 /** 288 346 * Counts the links. 347 * 348 * @since 0.3.0 289 349 * 290 350 * @return non-negative-int Link count. -
optimization-detective/trunk/class-od-tag-visitor-context.php
r3229869 r3240307 18 18 * @since 0.4.0 19 19 * 20 * @property-read OD_URL_Metric_Group_Collection $url_metrics_group_collection Deprecated property accessed via magic getter. Use the url_metric_group_collection property instead. 20 * @property-read OD_HTML_Tag_Processor $processor HTML tag processor. 21 * @property-read OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 22 * @property-read OD_Link_Collection $link_collection Link collection. 23 * @property-read positive-int|null $url_metrics_id ID for the od_url_metrics post which provided the URL Metrics in the collection. 24 * @property-read OD_URL_Metric_Group_Collection $url_metrics_group_collection Deprecated alias for the $url_metric_group_collection property. 21 25 */ 22 26 final class OD_Tag_Visitor_Context { … … 27 31 * @since 0.4.0 28 32 * @var OD_HTML_Tag_Processor 29 * @readonly30 33 */ 31 p ublic$processor;34 private $processor; 32 35 33 36 /** … … 36 39 * @since 0.4.0 37 40 * @var OD_URL_Metric_Group_Collection 38 * @readonly39 41 */ 40 p ublic$url_metric_group_collection;42 private $url_metric_group_collection; 41 43 42 44 /** … … 45 47 * @since 0.4.0 46 48 * @var OD_Link_Collection 47 * @readonly48 49 */ 49 public $link_collection; 50 private $link_collection; 51 52 /** 53 * ID for the od_url_metrics post which provided the URL Metrics in the collection. 54 * 55 * May be null if no post has been created yet. 56 * 57 * @since 1.0.0 58 * @var positive-int|null 59 */ 60 private $url_metrics_id; 50 61 51 62 /** 52 63 * Visited tag state. 64 * 65 * Important: This object is not exposed directly by the getter. It is only exposed via {@see self::track_tag()}. 53 66 * 54 67 * @since 1.0.0 … … 66 79 * @param OD_Link_Collection $link_collection Link collection. 67 80 * @param OD_Visited_Tag_State $visited_tag_state Visited tag state. 81 * @param positive-int|null $url_metrics_id ID for the od_url_metrics post which provided the URL Metrics in the collection. May be null if no post has been created yet. 68 82 */ 69 public function __construct( OD_HTML_Tag_Processor $processor, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_Link_Collection $link_collection, OD_Visited_Tag_State $visited_tag_state ) {83 public function __construct( OD_HTML_Tag_Processor $processor, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_Link_Collection $link_collection, OD_Visited_Tag_State $visited_tag_state, ?int $url_metrics_id ) { 70 84 $this->processor = $processor; 71 85 $this->url_metric_group_collection = $url_metric_group_collection; 72 86 $this->link_collection = $link_collection; 73 87 $this->visited_tag_state = $visited_tag_state; 88 $this->url_metrics_id = $url_metrics_id; 74 89 } 75 90 … … 86 101 87 102 /** 88 * Gets deprecatedproperty.103 * Gets a property. 89 104 * 90 105 * @since 0.7.0 91 * @todo Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore.92 106 * 93 107 * @param string $name Property name. 94 * @return OD_URL_Metric_Group_Collection URL Metric group collection.108 * @return mixed Property value. 95 109 * 96 110 * @throws Error When property is unknown. 97 111 */ 98 public function __get( string $name ): OD_URL_Metric_Group_Collection { 99 if ( 'url_metrics_group_collection' === $name ) { 100 _doing_it_wrong( 101 __CLASS__ . '::$url_metrics_group_collection', 102 esc_html( 103 sprintf( 104 /* translators: %s is class member variable name */ 105 __( 'Use %s instead.', 'optimization-detective' ), 106 __CLASS__ . '::$url_metric_group_collection' 112 public function __get( string $name ) { 113 // Note that there is intentionally not a case for 'visited_tag_state'. 114 switch ( $name ) { 115 case 'processor': 116 return $this->processor; 117 case 'link_collection': 118 return $this->link_collection; 119 case 'url_metrics_id': 120 return $this->url_metrics_id; 121 case 'url_metric_group_collection': 122 return $this->url_metric_group_collection; 123 case 'url_metrics_group_collection': 124 // TODO: Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore. 125 _doing_it_wrong( 126 esc_html( __CLASS__ . '::$' . $name ), 127 esc_html( 128 sprintf( 129 /* translators: %s is class member variable name */ 130 __( 'Use %s instead.', 'optimization-detective' ), 131 __CLASS__ . '::$url_metric_group_collection' 132 ) 133 ), 134 'optimization-detective 0.7.0' 135 ); 136 return $this->url_metric_group_collection; 137 default: 138 throw new Error( 139 esc_html( 140 sprintf( 141 /* translators: %s is class member variable name */ 142 __( 'Unknown property %s.', 'optimization-detective' ), 143 __CLASS__ . '::$' . $name 144 ) 107 145 ) 108 ), 109 'optimization-detective 0.7.0' 110 ); 111 return $this->url_metric_group_collection; 146 ); 112 147 } 113 throw new Error(114 esc_html(115 sprintf(116 /* translators: %s is class member variable name */117 __( 'Unknown property %s.', 'optimization-detective' ),118 __CLASS__ . '::$' . $name119 )120 )121 );122 148 } 123 149 } -
optimization-detective/trunk/class-od-tag-visitor-registry.php
r3229869 r3240307 21 21 * 22 22 * @since 0.3.0 23 * @access private24 23 */ 25 24 final class OD_Tag_Visitor_Registry implements Countable, IteratorAggregate { … … 28 27 * Visitors. 29 28 * 30 * @var array<string, TagVisitorCallback> 29 * @since 0.3.0 30 * 31 * @var array<non-empty-string, TagVisitorCallback> 31 32 */ 32 33 private $visitors = array(); … … 35 36 * Registers a tag visitor. 36 37 * 38 * @since 0.3.0 39 * 37 40 * @phpstan-param TagVisitorCallback $tag_visitor_callback 38 41 * 39 * @param string$id Identifier for the tag visitor.40 * @param callable $tag_visitor_callback Tag visitor callback.42 * @param non-empty-string $id Identifier for the tag visitor. 43 * @param callable $tag_visitor_callback Tag visitor callback. 41 44 */ 42 45 public function register( string $id, callable $tag_visitor_callback ): void { … … 47 50 * Determines if a visitor has been registered. 48 51 * 49 * @param string $id Identifier for the tag visitor. 52 * @since 0.3.0 53 * 54 * @param non-empty-string $id Identifier for the tag visitor. 50 55 * @return bool Whether registered. 51 56 */ … … 57 62 * Gets a registered visitor. 58 63 * 59 * @param string $id Identifier for the tag visitor. 64 * @since 0.3.0 65 * 66 * @param non-empty-string $id Identifier for the tag visitor. 60 67 * @return TagVisitorCallback|null Whether registered. 61 68 */ … … 70 77 * Unregisters a tag visitor. 71 78 * 72 * @param string $id Identifier for the tag visitor. 79 * @since 0.3.0 80 * 81 * @param non-empty-string $id Identifier for the tag visitor. 73 82 * @return bool Whether a tag visitor was unregistered. 74 83 */ … … 84 93 * Returns an iterator for the URL Metrics in the group. 85 94 * 95 * @since 0.3.0 96 * 86 97 * @return ArrayIterator<string, TagVisitorCallback> ArrayIterator for tag visitors. 87 98 */ … … 93 104 * Counts the URL Metrics in the group. 94 105 * 106 * @since 0.3.0 107 * 95 108 * @return int<0, max> URL Metric count. 96 109 */ -
optimization-detective/trunk/class-od-url-metric-group-collection.php
r3229869 r3240307 19 19 * 20 20 * @since 0.1.0 21 * @access private22 21 */ 23 22 final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggregate, JsonSerializable { … … 32 31 * in this case, in which every single URL Metric is added. 33 32 * 33 * @since 0.1.0 34 34 * @var OD_URL_Metric_Group[] 35 35 * @phpstan-var non-empty-array<OD_URL_Metric_Group> … … 48 48 * Breakpoints in max widths. 49 49 * 50 * Valid values are from 1 to PHP_INT_MAX - 1. This is because: 51 * 52 * 1. It doesn't make sense for there to be a viewport width of zero, so the first breakpoint (max width) must be at least 1. 53 * 2. After the last breakpoint, the final breakpoint group is set to be spanning one plus the last breakpoint max width up 54 * until PHP_INT_MAX. So a breakpoint cannot be PHP_INT_MAX because then the minimum viewport width for the final group 55 * would end up being larger than PHP_INT_MAX. 50 * A breakpoint must be greater than zero because a viewport group's maximum viewport width has a minimum (inclusive) 51 * value of 1, and the breakpoints are used as the maximum viewport widths for the viewport groups, with the addition of 52 * a final viewport group which has a maximum viewport width of infinity. 56 53 * 57 54 * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a 58 55 * single group. 59 56 * 60 * @ var int[]61 * @ phpstan-var positive-int[]57 * @since 0.1.0 58 * @var positive-int[] 62 59 */ 63 60 private $breakpoints; … … 66 63 * Sample size for URL Metrics for a given breakpoint. 67 64 * 68 * @ var int69 * @ phpstan-var positive-int65 * @since 0.1.0 66 * @var int<1, max> 70 67 */ 71 68 private $sample_size; … … 76 73 * A freshness age of zero means a URL Metric will always be considered stale. 77 74 * 78 * @ var int79 * @ phpstan-var 0|positive-int75 * @since 0.1.0 76 * @var int<0, max> 80 77 */ 81 78 private $freshness_ttl; … … 84 81 * Result cache. 85 82 * 83 * @since 0.3.0 86 84 * @var array{ 87 85 * get_group_for_viewport_width?: array<int, OD_URL_Metric_Group>, … … 92 90 * get_common_lcp_element?: OD_Element|null, 93 91 * get_all_element_max_intersection_ratios?: array<string, float>, 94 * get_xpath_elements_map?: array<string, non-empty-array< int, OD_Element>>,92 * get_xpath_elements_map?: array<string, non-empty-array<non-negative-int, OD_Element>>, 95 93 * get_all_elements_positioned_in_any_initial_viewport?: array<string, bool>, 96 94 * } … … 101 99 * Constructor. 102 100 * 101 * @since 0.1.0 102 * 103 103 * @throws InvalidArgumentException When an invalid argument is supplied. 104 * 105 * @phpstan-param positive-int[] $breakpoints 106 * @phpstan-param int<1, max> $sample_size 107 * @phpstan-param int<0, max> $freshness_ttl 104 108 * 105 109 * @param OD_URL_Metric[] $url_metrics URL Metrics. … … 128 132 $breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) ); 129 133 foreach ( $breakpoints as $breakpoint ) { 130 if ( ! is_int( $breakpoint ) || $breakpoint < 1 || PHP_INT_MAX === $breakpoint) {134 if ( ! is_int( $breakpoint ) || $breakpoint < 1 ) { 131 135 throw new InvalidArgumentException( 132 136 esc_html( … … 134 138 /* translators: %d is the invalid breakpoint */ 135 139 __( 136 'Each of the breakpoints must be greater than zero and less than PHP_INT_MAX, but encountered: %d',140 'Each of the breakpoints must be greater than zero, but encountered: %d', 137 141 'optimization-detective' 138 142 ), … … 197 201 198 202 /** 203 * Gets the breakpoints in max widths. 204 * 205 * @since 1.0.0 206 * 207 * @return positive-int[] Breakpoints in max widths. 208 */ 209 public function get_breakpoints(): array { 210 return $this->breakpoints; 211 } 212 213 /** 214 * Gets the sample size for URL Metrics for a given breakpoint. 215 * 216 * @since 1.0.0 217 * 218 * @return int<1, max> Sample size for URL Metrics for a given breakpoint. 219 */ 220 public function get_sample_size(): int { 221 return $this->sample_size; 222 } 223 224 /** 225 * Gets the freshness age (TTL) for a given URL Metric.. 226 * 227 * @since 1.0.0 228 * 229 * @return int<0, max> Freshness age (TTL) for a given URL Metric. 230 */ 231 public function get_freshness_ttl(): int { 232 return $this->freshness_ttl; 233 } 234 235 /** 199 236 * Gets the first URL Metric group. 200 237 * … … 216 253 * This group normally represents viewports for desktop devices. This group always has a minimum viewport width 217 254 * defined as one greater than the largest breakpoint returned by {@see od_get_breakpoint_max_widths()}. 218 * The maximum viewport is always `PHP_INT_MAX`, or in other words it is unbounded.255 * The maximum viewport width of this group is always `null`, or in other words it is unbounded. 219 256 * 220 257 * @since 0.7.0 … … 245 282 */ 246 283 private function create_groups(): array { 247 $groups = array();248 $min_width = 0;249 foreach ( $this->breakpoints as $max_width ) {250 $groups[] = new OD_URL_Metric_Group( array(), $min_width, $max_width, $this->sample_size, $this->freshness_ttl, $this );251 $min_width = $max_width + 1;252 } 253 $groups[] = new OD_URL_Metric_Group( array(), $min_width , PHP_INT_MAX, $this->sample_size, $this->freshness_ttl, $this );284 $groups = array(); 285 $min_width_exclusive = 0; 286 foreach ( $this->breakpoints as $max_width_inclusive ) { 287 $groups[] = new OD_URL_Metric_Group( array(), $min_width_exclusive, $max_width_inclusive, $this->sample_size, $this->freshness_ttl, $this ); 288 $min_width_exclusive = $max_width_inclusive; 289 } 290 $groups[] = new OD_URL_Metric_Group( array(), $min_width_exclusive, null, $this->sample_size, $this->freshness_ttl, $this ); 254 291 return $groups; 255 292 } … … 273 310 } 274 311 // @codeCoverageIgnoreStart 275 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX.312 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to an unbounded maximum. 276 313 throw new InvalidArgumentException( 277 314 esc_html__( 'No group available to add URL Metric to.', 'optimization-detective' ) … … 286 323 * @throws InvalidArgumentException When there is no group for the provided viewport width. This would only happen if a negative width is provided. 287 324 * 288 * @param int $viewport_width Viewport width.325 * @param positive-int $viewport_width Viewport width. 289 326 * @return OD_URL_Metric_Group URL Metric group for the viewport width. 290 327 */ … … 301 338 } 302 339 // @codeCoverageIgnoreStart 303 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to a maximum of PHP_INT_MAX.340 // In practice this exception should never get thrown because create_groups() creates groups from a minimum of 0 to an unbounded maximum. 304 341 throw new InvalidArgumentException( 305 342 esc_html( … … 496 533 * @since 0.7.0 497 534 * 498 * @return array<string, non-empty-array< int, OD_Element>> Keys are XPaths and values are the element instances.535 * @return array<string, non-empty-array<non-negative-int, OD_Element>> Keys are XPaths and values are the element instances. 499 536 */ 500 537 public function get_xpath_elements_map(): array { … … 668 705 * groups: array<int, array{ 669 706 * lcp_element: ?OD_Element, 670 * minimum_viewport_width: 0|positive-int,671 * maximum_viewport_width: positive-int,707 * minimum_viewport_width: int<0, max>, 708 * maximum_viewport_width: int<1, max>|null, 672 709 * complete: bool, 673 710 * url_metrics: OD_URL_Metric[] -
optimization-detective/trunk/class-od-url-metric-group.php
r3229869 r3240307 19 19 * 20 20 * @since 0.1.0 21 * @access private22 21 */ 23 22 final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSerializable { … … 33 32 34 33 /** 35 * Minimum possible viewport width for the group (inclusive). 36 * 37 * @since 0.1.0 38 * 39 * @var int 40 * @phpstan-var 0|positive-int 34 * Minimum possible viewport width for the group (exclusive). 35 * 36 * @since 0.1.0 37 * 38 * @var int<0, max> 41 39 */ 42 40 private $minimum_viewport_width; 43 41 44 42 /** 45 * Maximum possible viewport width for the group (inclusive). 46 * 47 * @since 0.1.0 48 * 49 * @var int 50 * @phpstan-var positive-int 43 * Maximum possible viewport width for the group (inclusive), where null means it is unbounded. 44 * 45 * @since 0.1.0 46 * 47 * @var int<1, max>|null 51 48 */ 52 49 private $maximum_viewport_width; … … 57 54 * @since 0.1.0 58 55 * 59 * @var int 60 * @phpstan-var positive-int 56 * @var int<1, max> 61 57 */ 62 58 private $sample_size; … … 67 63 * @since 0.1.0 68 64 * 69 * @var int 70 * @phpstan-var 0|positive-int 65 * @var int<0, max> 71 66 */ 72 67 private $freshness_ttl; … … 100 95 * This class should never be directly constructed. It should only be constructed by the {@see OD_URL_Metric_Group_Collection::create_groups()}. 101 96 * 97 * @since 0.1.0 98 * 102 99 * @access private 103 100 * @throws InvalidArgumentException If arguments are invalid. 104 101 * 102 * @phpstan-param int<0, max> $minimum_viewport_width 103 * @phpstan-param int<1, max>|null $maximum_viewport_width 104 * @phpstan-param int<1, max> $sample_size 105 * @phpstan-param int<0, max> $freshness_ttl 106 * 105 107 * @param OD_URL_Metric[] $url_metrics URL Metrics to add to the group. 106 * @param int $minimum_viewport_width Minimum possible viewport width for the group. Must be zero or greater.107 * @param int $maximum_viewport_width Maximum possible viewport width for the group. Must be greater than zero and the minimum viewport width.108 * @param int $minimum_viewport_width Minimum possible viewport width (exclusive) for the group. Must be zero or greater. 109 * @param int|null $maximum_viewport_width Maximum possible viewport width (inclusive) for the group. Must be greater than zero and the minimum viewport width. Null means unbounded. 108 110 * @param int $sample_size Sample size for the maximum number of viewports in a group between breakpoints. 109 111 * @param int $freshness_ttl Freshness age (TTL) for a given URL Metric. 110 112 * @param OD_URL_Metric_Group_Collection $collection Collection that this instance belongs to. 111 113 */ 112 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 ) {114 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 ) { 113 115 if ( $minimum_viewport_width < 0 ) { 114 116 throw new InvalidArgumentException( … … 116 118 ); 117 119 } 118 if ( $maximum_viewport_width < 1 ) { 119 throw new InvalidArgumentException( 120 esc_html__( 'The maximum viewport width must be greater than zero.', 'optimization-detective' ) 121 ); 122 } 123 if ( $minimum_viewport_width >= $maximum_viewport_width ) { 124 throw new InvalidArgumentException( 125 esc_html__( 'The minimum viewport width must be smaller than the maximum viewport width.', 'optimization-detective' ) 126 ); 120 if ( isset( $maximum_viewport_width ) ) { 121 if ( $maximum_viewport_width < 1 ) { 122 throw new InvalidArgumentException( 123 esc_html__( 'The maximum viewport width must be greater than zero.', 'optimization-detective' ) 124 ); 125 } 126 if ( $minimum_viewport_width >= $maximum_viewport_width ) { 127 throw new InvalidArgumentException( 128 esc_html__( 'The minimum viewport width must be smaller than the maximum viewport width.', 'optimization-detective' ) 129 ); 130 } 127 131 } 128 132 $this->minimum_viewport_width = $minimum_viewport_width; … … 159 163 160 164 /** 161 * Gets the minimum possible viewport width ( inclusive).165 * Gets the minimum possible viewport width (exclusive). 162 166 * 163 167 * @since 0.1.0 164 168 * 165 169 * @todo Eliminate in favor of readonly public property. 166 * @return int<0, max> Minimum viewport width .170 * @return int<0, max> Minimum viewport width (exclusive). 167 171 */ 168 172 public function get_minimum_viewport_width(): int { … … 176 180 * 177 181 * @todo Eliminate in favor of readonly public property. 178 * @return int<1, max> Minimum viewport width.179 */ 180 public function get_maximum_viewport_width(): int {182 * @return int<1, max>|null Minimum viewport width (inclusive). Null means unbounded. 183 */ 184 public function get_maximum_viewport_width(): ?int { 181 185 return $this->maximum_viewport_width; 182 186 } … … 188 192 * 189 193 * @todo Eliminate in favor of readonly public property. 190 * @phpstan-return positive-int 191 * @return int Sample size. 194 * @return int<1, max> Sample size. 192 195 */ 193 196 public function get_sample_size(): int { … … 201 204 * 202 205 * @todo Eliminate in favor of readonly public property. 203 * @phpstan-return 0|positive-int 204 * @return int Freshness age. 206 * @return int<0, max> Freshness age. 205 207 */ 206 208 public function get_freshness_ttl(): int { … … 209 211 210 212 /** 211 * Checks whether the provided viewport width is within the minimum/maximum range for. 212 * 213 * @since 0.1.0 213 * Gets the collection that this group is a part of. 214 * 215 * @since 1.0.0 216 * 217 * @todo Eliminate in favor of readonly public property. 218 * @return OD_URL_Metric_Group_Collection Collection. 219 */ 220 public function get_collection(): OD_URL_Metric_Group_Collection { 221 return $this->collection; 222 } 223 224 /** 225 * Checks whether the provided viewport width is between the minimum (exclusive) and maximum (inclusive). 226 * 227 * @since 0.1.0 228 * 229 * @phpstan-param int<1, max> $viewport_width 214 230 * 215 231 * @param int $viewport_width Viewport width. … … 218 234 public function is_viewport_width_in_range( int $viewport_width ): bool { 219 235 return ( 220 $viewport_width > =$this->minimum_viewport_width &&221 $viewport_width <= $this->maximum_viewport_width236 $viewport_width > $this->minimum_viewport_width && 237 ( null === $this->maximum_viewport_width || $viewport_width <= $this->maximum_viewport_width ) 222 238 ); 223 239 } … … 287 303 } 288 304 289 // The ETag is not populated yet, so this is stale. Eventually this will be required.290 if ( $url_metric->get_etag() === null ) {291 return false;292 }293 294 305 // The ETag of the URL Metric does not match the current ETag for the collection, so it is stale. 295 306 if ( ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) ) { … … 330 341 * Seen breadcrumbs counts. 331 342 * 332 * @var array<int, string> $seen_breadcrumbs343 * @var array<int, non-empty-string> $seen_breadcrumbs 333 344 */ 334 345 $seen_breadcrumbs = array(); … … 337 348 * Breadcrumb counts. 338 349 * 339 * @var array<int, int> $breadcrumb_counts350 * @var array<int, non-negative-int> $breadcrumb_counts 340 351 */ 341 352 $breadcrumb_counts = array(); … … 490 501 * freshness_ttl: 0|positive-int, 491 502 * sample_size: positive-int, 492 * minimum_viewport_width: 0|positive-int,493 * maximum_viewport_width: positive-int,503 * minimum_viewport_width: int<0, max>, 504 * maximum_viewport_width: int<1, max>|null, 494 505 * lcp_element: ?OD_Element, 495 506 * complete: bool, -
optimization-detective/trunk/class-od-url-metric.php
r3229869 r3240307 17 17 * 18 18 * @phpstan-type ViewportRect array{ 19 * width: int,20 * height: int19 * width: positive-int, 20 * height: positive-int 21 21 * } 22 22 * @phpstan-type DOMRect array{ … … 40 40 * @phpstan-type Data array{ 41 41 * uuid: non-empty-string, 42 * etag ?: non-empty-string,42 * etag: non-empty-string, 43 43 * url: non-empty-string, 44 44 * timestamp: float, … … 61 61 * 62 62 * @since 0.1.0 63 * @access private64 63 */ 65 64 class OD_URL_Metric implements JsonSerializable { … … 68 67 * Data. 69 68 * 69 * @since 0.1.0 70 70 * @var Data 71 71 */ … … 75 75 * Elements. 76 76 * 77 * @since 0.7.0 77 78 * @var OD_Element[] 78 79 */ … … 89 90 /** 90 91 * Constructor. 92 * 93 * @since 0.1.0 91 94 * 92 95 * @phpstan-param Data|array<string, mixed> $data Valid data or invalid data (in which case an exception is thrown). … … 105 108 /** 106 109 * Prepares data with validation and sanitization. 110 * 111 * @since 0.6.0 107 112 * 108 113 * @throws OD_Data_Validation_Exception When the input is invalid. … … 172 177 * @since 0.1.0 173 178 * @since 0.9.0 Added the 'etag' property to the schema. 179 * @since 1.0.0 The 'etag' property is now required. 174 180 * 175 181 * @todo Cache the return value? … … 231 237 'minLength' => 32, 232 238 'maxLength' => 32, 233 'required' => false, // To be made required in a future release.239 'required' => true, 234 240 'readonly' => true, // Omit from REST API. 235 241 ), … … 249 255 'type' => 'integer', 250 256 'required' => true, 251 'minimum' => 0,257 'minimum' => 1, 252 258 ), 253 259 'height' => array( 254 260 'type' => 'integer', 255 261 'required' => true, 256 'minimum' => 0,262 'minimum' => 1, 257 263 ), 258 264 ), … … 350 356 * @param array<string, mixed> $additional_properties Additional properties. 351 357 * @param string $filter_name Filter name used to extend. 352 *353 358 * @return array<string, mixed> Extended schema. 354 359 */ … … 437 442 * @since 0.6.0 438 443 * 439 * @return string UUID.444 * @return non-empty-string UUID. 440 445 */ 441 446 public function get_uuid(): string { … … 447 452 * 448 453 * @since 0.9.0 449 * 450 * @return non-empty-string|null ETag.451 * /452 public function get_etag(): ?string {453 // Since the ETag is optional for now, return null for old URL Metrics that do not have one.454 return $this->data['etag'] ?? null;454 * @since 1.0.0 No longer returns null as 'etag' is now required. 455 * 456 * @return non-empty-string ETag. 457 */ 458 public function get_etag(): string { 459 return $this->data['etag']; 455 460 } 456 461 … … 460 465 * @since 0.1.0 461 466 * 462 * @return string URL.467 * @return non-empty-string URL. 463 468 */ 464 469 public function get_url(): string { … … 482 487 * @since 0.1.0 483 488 * 484 * @return int Viewport width.489 * @return positive-int Viewport width. 485 490 */ 486 491 public function get_viewport_width(): int { -
optimization-detective/trunk/class-od-visited-tag-state.php
r3229869 r3240307 31 31 /** 32 32 * Constructor. 33 * 34 * @since 1.0.0 33 35 */ 34 36 public function __construct() { -
optimization-detective/trunk/detect.js
r3229869 r3240307 97 97 98 98 /** 99 * Checks whether the URL Metric(s) for the provided viewport width is needed. 99 * Gets the status for the URL Metric group for the provided viewport width. 100 * 101 * The comparison logic here corresponds with the PHP logic in `OD_URL_Metric_Group::is_viewport_width_in_range()`. 102 * This function is also similar to the PHP logic in `\OD_URL_Metric_Group_Collection::get_group_for_viewport_width()`. 100 103 * 101 104 * @param {number} viewportWidth - Current viewport width. 102 105 * @param {URLMetricGroupStatus[]} urlMetricGroupStatuses - Viewport group statuses. 103 * @return {boolean} Whether URL Metrics are needed. 104 */ 105 function isViewportNeeded( viewportWidth, urlMetricGroupStatuses ) { 106 let lastWasLacking = false; 107 for ( const { minimumViewportWidth, complete } of urlMetricGroupStatuses ) { 108 if ( viewportWidth >= minimumViewportWidth ) { 109 lastWasLacking = ! complete; 110 } else { 111 break; 112 } 113 } 114 return lastWasLacking; 106 * @return {URLMetricGroupStatus} The URL metric group for the viewport width. 107 */ 108 function getGroupForViewportWidth( viewportWidth, urlMetricGroupStatuses ) { 109 for ( const urlMetricGroupStatus of urlMetricGroupStatuses ) { 110 if ( 111 viewportWidth > urlMetricGroupStatus.minimumViewportWidth && 112 ( null === urlMetricGroupStatus.maximumViewportWidth || 113 viewportWidth <= urlMetricGroupStatus.maximumViewportWidth ) 114 ) { 115 return urlMetricGroupStatus; 116 } 117 } 118 throw new Error( 119 `${ consoleLogPrefix } Unexpectedly unable to locate group for the current viewport width.` 120 ); 121 } 122 123 /** 124 * Gets the sessionStorage key for keeping track of whether the current client session already submitted a URL Metric. 125 * 126 * @param {string} currentETag - Current ETag. 127 * @param {string} currentUrl - Current URL. 128 * @param {URLMetricGroupStatus} urlMetricGroupStatus - URL Metric group status. 129 * @return {Promise<string>} Session storage key. 130 */ 131 async function getAlreadySubmittedSessionStorageKey( 132 currentETag, 133 currentUrl, 134 urlMetricGroupStatus 135 ) { 136 const message = [ 137 currentETag, 138 currentUrl, 139 urlMetricGroupStatus.minimumViewportWidth, 140 urlMetricGroupStatus.maximumViewportWidth || '', 141 ].join( '-' ); 142 143 /* 144 * Note that the components are hashed for a couple of reasons: 145 * 146 * 1. It results in a consistent length string devoid of any special characters that could cause problems. 147 * 2. Since the key includes the URL, hashing it avoids potential privacy concerns where the sessionStorage is 148 * examined to see which URLs the client went to. 149 * 150 * The SHA-1 algorithm is chosen since it is the fastest and there is no need for cryptographic security. 151 */ 152 const msgBuffer = new TextEncoder().encode( message ); 153 const hashBuffer = await crypto.subtle.digest( 'SHA-1', msgBuffer ); 154 const hashHex = Array.from( new Uint8Array( hashBuffer ) ) 155 .map( ( b ) => b.toString( 16 ).padStart( 2, '0' ) ) 156 .join( '' ); 157 return `odSubmitted-${ hashHex }`; 115 158 } 116 159 … … 254 297 * @param {boolean} args.isDebug Whether to show debug messages. 255 298 * @param {string} args.restApiEndpoint URL for where to send the detection data. 299 * @param {string} [args.restApiNonce] Nonce for the REST API when the user is logged-in. 256 300 * @param {string} args.currentETag Current ETag. 257 301 * @param {string} args.currentUrl Current URL. … … 261 305 * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses. 262 306 * @param {number} args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock. 307 * @param {number} args.freshnessTTL The freshness age (TTL) for a given URL Metric. 263 308 * @param {string} args.webVitalsLibrarySrc The URL for the web-vitals library. 264 309 * @param {CollectionDebugData} [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode. … … 270 315 extensionModuleUrls, 271 316 restApiEndpoint, 317 restApiNonce, 272 318 currentETag, 273 319 currentUrl, … … 277 323 urlMetricGroupStatuses, 278 324 storageLockTTL, 325 freshnessTTL, 279 326 webVitalsLibrarySrc, 280 327 urlMetricGroupCollection, … … 298 345 } 299 346 347 if ( win.innerWidth === 0 || win.innerHeight === 0 ) { 348 if ( isDebug ) { 349 log( 350 'Window must have non-zero dimensions for URL Metric collection.' 351 ); 352 } 353 return; 354 } 355 300 356 // Abort if the current viewport is not among those which need URL Metrics. 301 if ( ! isViewportNeeded( win.innerWidth, urlMetricGroupStatuses ) ) { 357 const urlMetricGroupStatus = getGroupForViewportWidth( 358 win.innerWidth, 359 urlMetricGroupStatuses 360 ); 361 if ( urlMetricGroupStatus.complete ) { 302 362 if ( isDebug ) { 303 363 log( 'No need for URL Metrics from the current viewport.' ); 304 364 } 305 365 return; 366 } 367 368 // Abort if the client already submitted a URL Metric for this URL and viewport group. 369 const alreadySubmittedSessionStorageKey = 370 await getAlreadySubmittedSessionStorageKey( 371 currentETag, 372 currentUrl, 373 urlMetricGroupStatus 374 ); 375 if ( alreadySubmittedSessionStorageKey in sessionStorage ) { 376 const previousVisitTime = parseInt( 377 sessionStorage.getItem( alreadySubmittedSessionStorageKey ), 378 10 379 ); 380 if ( 381 ! isNaN( previousVisitTime ) && 382 ( getCurrentTime() - previousVisitTime ) / 1000 < freshnessTTL 383 ) { 384 if ( isDebug ) { 385 log( 386 'The current client session already submitted a fresh URL Metric for this URL so a new one will not be collected now.' 387 ); 388 return; 389 } 390 } 306 391 } 307 392 … … 604 689 } 605 690 691 // Finalize extensions. 606 692 if ( extensions.size > 0 ) { 607 693 /** @type {Promise[]} */ … … 656 742 } 657 743 744 /* 745 * Now prepare the URL Metric to be sent as JSON request body. 746 */ 747 748 const maxBodyLengthKiB = 64; 749 const maxBodyLengthBytes = maxBodyLengthKiB * 1024; 750 751 // TODO: Consider adding replacer to reduce precision on numbers in DOMRect to reduce payload size. 752 const jsonBody = JSON.stringify( urlMetric ); 753 const percentOfBudget = 754 ( jsonBody.length / ( maxBodyLengthKiB * 1000 ) ) * 100; 755 756 /* 757 * According to the fetch() spec: 758 * "If the sum of contentLength and inflightKeepaliveBytes is greater than 64 kibibytes, then return a network error." 759 * This is what browsers also implement for navigator.sendBeacon(). Therefore, if the size of the JSON is greater 760 * than the maximum, we should avoid even trying to send it. 761 */ 762 if ( jsonBody.length > maxBodyLengthBytes ) { 763 if ( isDebug ) { 764 error( 765 `Unable to send URL Metric because it is ${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( 766 percentOfBudget 767 ) }% of ${ maxBodyLengthKiB } KiB limit:`, 768 urlMetric 769 ); 770 } 771 return; 772 } 773 658 774 // Even though the server may reject the REST API request, we still have to set the storage lock 659 775 // because we can't look at the response when sending a beacon. 660 776 setStorageLock( getCurrentTime() ); 661 777 778 // Remember that the URL Metric was submitted for this URL to avoid having multiple entries submitted by the same client. 779 sessionStorage.setItem( 780 alreadySubmittedSessionStorageKey, 781 String( getCurrentTime() ) 782 ); 783 662 784 if ( isDebug ) { 663 log( 'Sending URL Metric:', urlMetric ); 785 const message = `Sending URL Metric (${ jsonBody.length.toLocaleString() } bytes, ${ Math.round( 786 percentOfBudget 787 ) }% of ${ maxBodyLengthKiB } KiB limit):`; 788 789 // The threshold of 50% is used because the limit for all beacons combined is 64 KiB, not just the data for one beacon. 790 if ( percentOfBudget < 50 ) { 791 log( message, urlMetric ); 792 } else { 793 warn( message, urlMetric ); 794 } 664 795 } 665 796 666 797 const url = new URL( restApiEndpoint ); 798 if ( typeof restApiNonce === 'string' ) { 799 url.searchParams.set( '_wpnonce', restApiNonce ); 800 } 667 801 url.searchParams.set( 'slug', urlMetricSlug ); 668 802 url.searchParams.set( 'current_etag', currentETag ); … … 676 810 navigator.sendBeacon( 677 811 url, 678 new Blob( [ JSON.stringify( urlMetric )], {812 new Blob( [ jsonBody ], { 679 813 type: 'application/json', 680 814 } ) -
optimization-detective/trunk/detect.min.js
r3210025 r3240307 1 const win=window,doc=win.document,consoleLogPrefix="[Optimization Detective]",storageLockTimeSessionKey="odStorageLockTime";function isStorageLocked(e,t){if(0===t)return!1;try{const o=parseInt(sessionStorage.getItem("odStorageLockTime"));return!isNaN(o)&&e<o+1e3*t}catch(e){return!1}}function setStorageLock(e){try{sessionStorage.setItem("odStorageLockTime",String(e))}catch(e){}}function log(...e){console.log(consoleLogPrefix,...e)}function warn(...e){console.warn(consoleLogPrefix,...e)}function error(...e){console.error(consoleLogPrefix,...e)}function isViewportNeeded(e,t){let o=!1;for(const{minimumViewportWidth:n,complete:r}of t){if(!(e>=n))break;o=!r}return o}function getCurrentTime(){return Date.now()}function recursiveFreeze(e){for(const t of Object.getOwnPropertyNames(e)){const o=e[t];null!==o&&"object"==typeof o&&recursiveFreeze(o)}Object.freeze(e)}let urlMetric;const reservedRootPropertyKeys=new Set(["url","viewport","elements"]);function getRootData(){const e=structuredClone(urlMetric);return recursiveFreeze(e),e}function extendRootData(e){for(const t of Object.getOwnPropertyNames(e))if(reservedRootPropertyKeys.has(t))throw new Error(`Disallowed setting of key '${t}' on root.`);Object.assign(urlMetric,e)}const elementsByXPath=new Map,reservedElementPropertyKeys=new Set(["isLCP","isLCPCandidate","xpath","intersectionRatio","intersectionRect","boundingClientRect"]);function getElementData(e){const t=elementsByXPath.get(e);if(t){const e=structuredClone(t);return recursiveFreeze(e),e}return null}function extendElementData(e,t){if(!elementsByXPath.has(e))throw new Error(`Unknown element with XPath: ${e}`);for(const e of Object.getOwnPropertyNames(t))if(reservedElementPropertyKeys.has(e))throw new Error(`Disallowed setting of key '${e}' on element.`);const o=elementsByXPath.get(e);Object.assign(o,t)}export default async function detect({minViewportAspectRatio:e,maxViewportAspectRatio:t,isDebug:o,extensionModuleUrls:n,restApiEndpoint:r,currentETag:i,currentUrl:s,urlMetricSlug:a,cachePurgePostId:c,urlMetricHMAC:l,urlMetricGroupStatuses:d,storageLockTTL:u,webVitalsLibrarySrc:g,urlMetricGroupCollection:f}){if(o){const e=[];for(const t of f.groups)for(const o of t.url_metrics)o.creationDate=new Date(1e3*o.timestamp),e.push(o);log("Stored URL Metric Group Collection:",f),e.sort(((e,t)=>t.timestamp-e.timestamp)),log("Stored URL Metrics in reverse chronological order:",e)}if(!isViewportNeeded(win.innerWidth,d))return void(o&&log("No need for URL Metrics from the current viewport."));const p=win.innerWidth/win.innerHeight;if(p<e||p>t)return void(o&&warn(`Viewport aspect ratio (${p}) is not in the accepted range of ${e} to ${t}.`));if(await new Promise((e=>{"loading"!==doc.readyState?e():doc.addEventListener("DOMContentLoaded",e,{once:!0})})),await new Promise((e=>{"complete"===doc.readyState?e():win.addEventListener("load",e,{once:!0})})),"function"==typeof requestIdleCallback&&await new Promise((e=>{requestIdleCallback(e)})),isStorageLocked(getCurrentTime(),u))return void(o&&warn("Aborted detection due to storage being locked."));let w=!1;window.addEventListener("resize",(()=>{w=!0}),{once:!0});const{onTTFB:m,onFCP:h,onLCP:P,onINP:L,onCLS:y}=await import(g);if(doc.documentElement.scrollTop>0)return void(o&&warn("Aborted detection since initial scroll position of page is not at the top."));o&&log("Proceeding with detection");const v=new Map,b=[],S=[];for(const e of n)try{const t=await import(e);if(v.set(e,t),t.initialize instanceof Function){const n=t.initialize({isDebug:o,onTTFB:m,onFCP:h,onLCP:P,onINP:L,onCLS:y});n instanceof Promise&&(b.push(n),S.push(e))}}catch(t){error(`Failed to start initializing extension '${e}':`,t)}const C=await Promise.allSettled(b);for(const[e,t]of C.entries())"rejected"===t.status&&error(`Failed to initialize extension '${S[e]}':`,t.reason);const R=doc.body.querySelectorAll("[data-od-xpath]"),M=new Map([...R].map((e=>[e,e.dataset.odXpath]))),D=[];let E;function x(){E instanceof IntersectionObserver&&(E.disconnect(),win.removeEventListener("scroll",x))}M.size>0&&(await new Promise((e=>{E=new IntersectionObserver((t=>{for(const e of t)D.push(e);e()}),{root:null,threshold:0});for(const e of M.keys())E.observe(e)})),win.addEventListener("scroll",x,{once:!0,passive:!0}));const k=[];await new Promise((e=>{P((t=>{k.push(t),e()}),{reportAllChanges:!0})})),x(),o&&log("Detection is stopping."),urlMetric={url:s,viewport:{width:win.innerWidth,height:win.innerHeight},elements:[]};const z=k.at(-1);for(const e of D){const t=M.get(e.target);if(!t){o&&error("Unable to look up XPath for element");continue}const n=z?.entries[0]?.element,r={isLCP:e.target===n,isLCPCandidate:!!k.find((t=>{const o=t.entries[0]?.element;return o===e.target})),xpath:t,intersectionRatio:e.intersectionRatio,intersectionRect:e.intersectionRect,boundingClientRect:e.boundingClientRect};urlMetric.elements.push(r),elementsByXPath.set(r.xpath,r)}if(o&&log("Current URL Metric:",urlMetric),await new Promise((e=>{win.addEventListener("pagehide",e,{once:!0}),win.addEventListener("pageswap",e,{once:!0}),doc.addEventListener("visibilitychange",(()=>{"hidden"===document.visibilityState&&e()}),{once:!0})})),w)return void(o&&log("Aborting URL Metric collection due to viewport size change."));if(v.size>0){const e=[],t=[];for(const[n,r]of v.entries())if(r.finalize instanceof Function)try{const i=r.finalize({isDebug:o,getRootData,getElementData,extendElementData,extendRootData});i instanceof Promise&&(e.push(i),t.push(n))}catch(e){error(`Unable to start finalizing extension '${n}':`,e)}const n=await Promise.allSettled(e);for(const[e,o]of n.entries())"rejected"===o.status&&error(`Failed to finalize extension '${t[e]}':`,o.reason)}setStorageLock(getCurrentTime()),o&&log("Sending URL Metric:",urlMetric);const T=new URL(r);T.searchParams.set("slug",a),T.searchParams.set("current_etag",i),"number"==typeof c&&T.searchParams.set("cache_purge_post_id",c.toString()),T.searchParams.set("hmac",l),navigator.sendBeacon(T,new Blob([JSON.stringify(urlMetric)],{type:"application/json"})),M.clear()}1 const win=window,doc=win.document,consoleLogPrefix="[Optimization Detective]",storageLockTimeSessionKey="odStorageLockTime";function isStorageLocked(e,t){if(0===t)return!1;try{const o=parseInt(sessionStorage.getItem("odStorageLockTime"));return!isNaN(o)&&e<o+1e3*t}catch(e){return!1}}function setStorageLock(e){try{sessionStorage.setItem("odStorageLockTime",String(e))}catch(e){}}function log(...e){console.log(consoleLogPrefix,...e)}function warn(...e){console.warn(consoleLogPrefix,...e)}function error(...e){console.error(consoleLogPrefix,...e)}function getGroupForViewportWidth(e,t){for(const o of t)if(e>o.minimumViewportWidth&&(null===o.maximumViewportWidth||e<=o.maximumViewportWidth))return o;throw new Error(`${consoleLogPrefix} Unexpectedly unable to locate group for the current viewport width.`)}async function getAlreadySubmittedSessionStorageKey(e,t,o){const n=[e,t,o.minimumViewportWidth,o.maximumViewportWidth||""].join("-"),r=(new TextEncoder).encode(n),i=await crypto.subtle.digest("SHA-1",r);return`odSubmitted-${Array.from(new Uint8Array(i)).map((e=>e.toString(16).padStart(2,"0"))).join("")}`}function getCurrentTime(){return Date.now()}function recursiveFreeze(e){for(const t of Object.getOwnPropertyNames(e)){const o=e[t];null!==o&&"object"==typeof o&&recursiveFreeze(o)}Object.freeze(e)}let urlMetric;const reservedRootPropertyKeys=new Set(["url","viewport","elements"]);function getRootData(){const e=structuredClone(urlMetric);return recursiveFreeze(e),e}function extendRootData(e){for(const t of Object.getOwnPropertyNames(e))if(reservedRootPropertyKeys.has(t))throw new Error(`Disallowed setting of key '${t}' on root.`);Object.assign(urlMetric,e)}const elementsByXPath=new Map,reservedElementPropertyKeys=new Set(["isLCP","isLCPCandidate","xpath","intersectionRatio","intersectionRect","boundingClientRect"]);function getElementData(e){const t=elementsByXPath.get(e);if(t){const e=structuredClone(t);return recursiveFreeze(e),e}return null}function extendElementData(e,t){if(!elementsByXPath.has(e))throw new Error(`Unknown element with XPath: ${e}`);for(const e of Object.getOwnPropertyNames(t))if(reservedElementPropertyKeys.has(e))throw new Error(`Disallowed setting of key '${e}' on element.`);const o=elementsByXPath.get(e);Object.assign(o,t)}export default async function detect({minViewportAspectRatio:e,maxViewportAspectRatio:t,isDebug:o,extensionModuleUrls:n,restApiEndpoint:r,restApiNonce:i,currentETag:s,currentUrl:a,urlMetricSlug:c,cachePurgePostId:l,urlMetricHMAC:d,urlMetricGroupStatuses:u,storageLockTTL:g,freshnessTTL:f,webVitalsLibrarySrc:m,urlMetricGroupCollection:w}){if(o){const e=[];for(const t of w.groups)for(const o of t.url_metrics)o.creationDate=new Date(1e3*o.timestamp),e.push(o);log("Stored URL Metric Group Collection:",w),e.sort(((e,t)=>t.timestamp-e.timestamp)),log("Stored URL Metrics in reverse chronological order:",e)}if(0===win.innerWidth||0===win.innerHeight)return void(o&&log("Window must have non-zero dimensions for URL Metric collection."));const p=getGroupForViewportWidth(win.innerWidth,u);if(p.complete)return void(o&&log("No need for URL Metrics from the current viewport."));const h=await getAlreadySubmittedSessionStorageKey(s,a,p);if(h in sessionStorage){const e=parseInt(sessionStorage.getItem(h),10);if(!isNaN(e)&&(getCurrentTime()-e)/1e3<f&&o)return void log("The current client session already submitted a fresh URL Metric for this URL so a new one will not be collected now.")}const y=win.innerWidth/win.innerHeight;if(y<e||y>t)return void(o&&warn(`Viewport aspect ratio (${y}) is not in the accepted range of ${e} to ${t}.`));if(await new Promise((e=>{"loading"!==doc.readyState?e():doc.addEventListener("DOMContentLoaded",e,{once:!0})})),await new Promise((e=>{"complete"===doc.readyState?e():win.addEventListener("load",e,{once:!0})})),"function"==typeof requestIdleCallback&&await new Promise((e=>{requestIdleCallback(e)})),isStorageLocked(getCurrentTime(),g))return void(o&&warn("Aborted detection due to storage being locked."));let L=!1;window.addEventListener("resize",(()=>{L=!0}),{once:!0});const{onTTFB:P,onFCP:S,onLCP:b,onINP:v,onCLS:M}=await import(m);if(doc.documentElement.scrollTop>0)return void(o&&warn("Aborted detection since initial scroll position of page is not at the top."));o&&log("Proceeding with detection");const C=new Map,R=[],x=[];for(const e of n)try{const t=await import(e);if(C.set(e,t),t.initialize instanceof Function){const n=t.initialize({isDebug:o,onTTFB:P,onFCP:S,onLCP:b,onINP:v,onCLS:M});n instanceof Promise&&(R.push(n),x.push(e))}}catch(t){error(`Failed to start initializing extension '${e}':`,t)}const E=await Promise.allSettled(R);for(const[e,t]of E.entries())"rejected"===t.status&&error(`Failed to initialize extension '${x[e]}':`,t.reason);const D=doc.body.querySelectorAll("[data-od-xpath]"),T=new Map([...D].map((e=>[e,e.dataset.odXpath]))),z=[];let U;function k(){U instanceof IntersectionObserver&&(U.disconnect(),win.removeEventListener("scroll",k))}T.size>0&&(await new Promise((e=>{U=new IntersectionObserver((t=>{for(const e of t)z.push(e);e()}),{root:null,threshold:0});for(const e of T.keys())U.observe(e)})),win.addEventListener("scroll",k,{once:!0,passive:!0}));const $=[];await new Promise((e=>{b((t=>{$.push(t),e()}),{reportAllChanges:!0})})),k(),o&&log("Detection is stopping."),urlMetric={url:a,viewport:{width:win.innerWidth,height:win.innerHeight},elements:[]};const A=$.at(-1);for(const e of z){const t=T.get(e.target);if(!t){o&&error("Unable to look up XPath for element");continue}const n=A?.entries[0]?.element,r={isLCP:e.target===n,isLCPCandidate:!!$.find((t=>{const o=t.entries[0]?.element;return o===e.target})),xpath:t,intersectionRatio:e.intersectionRatio,intersectionRect:e.intersectionRect,boundingClientRect:e.boundingClientRect};urlMetric.elements.push(r),elementsByXPath.set(r.xpath,r)}if(o&&log("Current URL Metric:",urlMetric),await new Promise((e=>{win.addEventListener("pagehide",e,{once:!0}),win.addEventListener("pageswap",e,{once:!0}),doc.addEventListener("visibilitychange",(()=>{"hidden"===document.visibilityState&&e()}),{once:!0})})),L)return void(o&&log("Aborting URL Metric collection due to viewport size change."));if(C.size>0){const e=[],t=[];for(const[n,r]of C.entries())if(r.finalize instanceof Function)try{const i=r.finalize({isDebug:o,getRootData,getElementData,extendElementData,extendRootData});i instanceof Promise&&(e.push(i),t.push(n))}catch(e){error(`Unable to start finalizing extension '${n}':`,e)}const n=await Promise.allSettled(e);for(const[e,o]of n.entries())"rejected"===o.status&&error(`Failed to finalize extension '${t[e]}':`,o.reason)}const F=JSON.stringify(urlMetric),O=F.length/64e3*100;if(F.length>65536)return void(o&&error(`Unable to send URL Metric because it is ${F.length.toLocaleString()} bytes, ${Math.round(O)}% of 64 KiB limit:`,urlMetric));if(setStorageLock(getCurrentTime()),sessionStorage.setItem(h,String(getCurrentTime())),o){const e=`Sending URL Metric (${F.length.toLocaleString()} bytes, ${Math.round(O)}% of 64 KiB limit):`;O<50?log(e,urlMetric):warn(e,urlMetric)}const I=new URL(r);"string"==typeof i&&I.searchParams.set("_wpnonce",i),I.searchParams.set("slug",c),I.searchParams.set("current_etag",s),"number"==typeof l&&I.searchParams.set("cache_purge_post_id",l.toString()),I.searchParams.set("hmac",d),navigator.sendBeacon(I,new Blob([F],{type:"application/json"})),T.clear()} -
optimization-detective/trunk/detection.php
r3229869 r3240307 39 39 * @global WP_Query $wp_query WordPress Query object. 40 40 * 41 * @return int|null Post ID or null if none found.41 * @return positive-int|null Post ID or null if none found. 42 42 */ 43 43 function od_get_cache_purge_post_id(): ?int { 44 44 $queried_object = get_queried_object(); 45 if ( $queried_object instanceof WP_Post ) {45 if ( $queried_object instanceof WP_Post && $queried_object->ID > 0 ) { 46 46 return $queried_object->ID; 47 47 } … … 56 56 && 57 57 $wp_query->posts[0] instanceof WP_Post 58 && 59 $wp_query->posts[0]->ID > 0 58 60 ) { 59 61 return $wp_query->posts[0]->ID; … … 69 71 * @access private 70 72 * 71 * @param string$slug URL Metrics slug.73 * @param non-empty-string $slug URL Metrics slug. 72 74 * @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection. 73 75 */ … … 129 131 static function ( OD_URL_Metric_Group $group ): array { 130 132 return array( 131 'minimumViewportWidth' => $group->get_minimum_viewport_width(), 133 'minimumViewportWidth' => $group->get_minimum_viewport_width(), // Exclusive. 134 'maximumViewportWidth' => $group->get_maximum_viewport_width(), // Inclusive. 132 135 'complete' => $group->is_complete(), 133 136 ); … … 136 139 ), 137 140 'storageLockTTL' => OD_Storage_Lock::get_ttl(), 141 'freshnessTTL' => od_get_url_metric_freshness_ttl(), 138 142 'webVitalsLibrarySrc' => $web_vitals_lib_src, 139 143 ); 144 if ( is_user_logged_in() ) { 145 $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' ); 146 } 140 147 if ( WP_DEBUG ) { 141 148 $detect_args['urlMetricGroupCollection'] = $group_collection; -
optimization-detective/trunk/docs/extensions.md
r3229869 r3240307 11 11 **[Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) ([GitHub](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer)):** 12 12 13 1. Add breakpoint-specific `fetchpriority=high` preload links ( `LINK[rel=preload]`) for image URLs of LCP elements:13 1. Add breakpoint-specific `fetchpriority=high` preload links (both as `LINK[rel=preload]` elements and `Link` response headers) for image URLs of LCP elements: 14 14 1. An `IMG` element, including the `srcset`/`sizes` attributes supplied as `imagesrcset`/`imagesizes` on the `LINK`. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L167-L177), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L304-L349)) 15 15 2. The first `SOURCE` element with a `type` attribute in a `PICTURE` element. (Art-directed `PICTURE` elements using media queries are not supported.) ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L192-L275), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L304-L349)) … … 23 23 2. Implement lazy loading of CSS background images added via inline `style` attributes. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-background-image-styled-tag-visitor.php#L205-L238), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L365-L380), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/lazy-load-bg-image.js)) 24 24 3. Lazy-load `VIDEO` tags by setting the appropriate attributes based on whether they appear in the initial viewport. If a `VIDEO` is the LCP element, it gets `preload=auto`; if it is in an initial viewport, the `preload=metadata` default is left; if it is not in an initial viewport, it gets `preload=none`. Lazy-loaded videos also get initial `preload`, `autoplay`, and `poster` attributes restored when the `VIDEO` is going to enter the viewport. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php#L163-L246), [2](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/helper.php#L365-L380), [3](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/lazy-load-video.js)) 25 5. Ensure that [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is added to all lazy-loaded `IMG` elements. ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L148-L163)) 25 5. Responsive image sizes: 26 1. Compute the `sizes` attribute using the widths of an image collected from URL Metrics for each breakpoint (when not lazy-loaded since then handled by `sizes=auto`). ([1](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L170-L184), [2](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L412-L444)) 27 2. Ensure [`sizes=auto`](https://make.wordpress.org/core/2024/10/18/auto-sizes-for-lazy-loaded-images-in-wordpress-6-7/) is set on `IMG` tags after setting correct lazy-loading (above). ([1](https://github.com/WordPress/performance/blob/6459571471b26aee4f63f00e2ba9dfe6f5ce2f39/plugins/image-prioritizer/class-image-prioritizer-img-tag-visitor.php#L156-L168)) 26 28 6. Reduce the size of the `poster` image of a `VIDEO` from full size to the size appropriate for the maximum width of the video (on desktop). ([1](https://github.com/WordPress/performance/blob/f5f50f9179c26deadeef966734367d199ba6de6f/plugins/image-prioritizer/class-image-prioritizer-video-tag-visitor.php#L84-L125)) 27 29 -
optimization-detective/trunk/docs/hooks.md
r3229869 r3240307 103 103 ``` 104 104 105 ### Filter: `od_url_metric_storage_lock_ttl` (default: 1 minute in seconds) 106 107 Filters how long a given IP is locked from submitting another metric-storage REST API request. Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: 105 ### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for authorized logged-in users) 106 107 Filters how long the current IP is locked from submitting another URL metric storage REST API request. 108 109 Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following: 108 110 109 111 ```php … … 113 115 ``` 114 116 117 By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. 118 115 119 During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release: 116 120 … … 121 125 ``` 122 126 123 ### Filter: `od_url_metric_freshness_ttl` (default: 1 dayin seconds)124 125 Filters the freshness age (TTL) for a given URL Metric. The freshness TTL must be at least zero, in which it considers URL Metrics to always be stale. In practice, the value should be at least an hour. If your site content does not change frequently, you may want to increase the TTL to a week:127 ### Filter: `od_url_metric_freshness_ttl` (default: 1 week in seconds) 128 129 Filters the freshness age (TTL) for a given URL Metric. The freshness TTL must be at least zero, in which it considers URL Metrics to always be stale. In practice, the value should be at least an hour. If your site content does not change frequently, you may want to increase the TTL even longer, say to a month: 126 130 127 131 ```php 128 132 add_filter( 'od_url_metric_freshness_ttl', static function (): int { 129 return WEEK_IN_SECONDS; 130 } ); 131 ``` 133 return MONTH_IN_SECONDS; 134 } ); 135 ``` 136 137 Note that even if you have large freshness TTL a URL Metric can still become stale sooner; if the page state changes then this results in a change to the ETag associated with a URL Metric. This will allow new URL Metrics to be collected before the freshness TTL has transpired. See the `od_current_url_metrics_etag_data` filter to customize the ETag data. 132 138 133 139 During development, this can be useful to set to zero so that you don't have to wait for new URL Metrics to be requested when engineering a new optimization: … … 230 236 See also [example usage](https://github.com/WordPress/performance/blob/6bb8405c5c446e3b66c2bfa3ae03ba61b188bca2/plugins/embed-optimizer/hooks.php#L128-L144) in Embed Optimizer. Note in particular the structure of the plugin’s [detect.js](https://github.com/WordPress/performance/blob/trunk/plugins/embed-optimizer/detect.js) script module, how it exports `initialize` and `finalize` functions which Optimization Detective then calls when the page loads and when the page unloads, at which time the URL Metric is constructed and sent to the server for storage. Refer also to the [TypeScript type definitions](https://github.com/WordPress/performance/blob/trunk/plugins/optimization-detective/types.ts). 231 237 232 ### Filter: `od_current_url_metrics_etag_data` (default: array with `tag_visitors` key)238 ### Filter: `od_current_url_metrics_etag_data` (default: `array<string, mixed>`) 233 239 234 240 Filters the data that goes into computing the current ETag for URL Metrics. 235 241 236 The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes the names of registered tag visitors. This ensures that when a new Optimization Detective-dependent plugin is activated (like [Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) or [Embed Optimizer](https://wordpress.org/plugins/embed-optimizer/)), any existing URL Metrics are immediately considered stale. This happens because the newly registered tag visitors alter the ETag calculation, making it different from the stored ones. 237 238 When the ETag for URL Metrics in a complete viewport group no longer matches the current environment's ETag, new URL Metrics will then begin to be collected until there are no more stored URL Metrics with the old ETag. These new URL Metrics will include data relevant to the newly activated plugins and their tag visitors. 242 The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes: 243 244 1. The active theme and current version (for both parent and child themes). 245 2. The queried object ID, post type, and modified date. 246 3. The list of registered tag visitors. 247 4. The IDs and modified times of posts in The Loop. 248 5. The current theme template used to render the page. 249 6. The list of active plugins. 250 251 A change in ETag means that any previously-collected URL Metrics will be immediately considered stale. When the ETag for URL Metrics in a complete viewport group no longer matches the current environment's ETag, new URL Metrics will then begin to be collected until there are no more stored URL Metrics with the old ETag. -
optimization-detective/trunk/helper.php
r3229869 r3240307 23 23 * Fires when extensions to Optimization Detective can be loaded and initialized. 24 24 * 25 * This action is useful for loading extension code that depends on Optimization Detective to be running. The version 26 * of the plugin is passed as the sole argument so that if the required version is not present, the callback can short circuit. 27 * 28 * Example: 29 * 30 * add_action( 'od_init', function ( string $version ) { 31 * if ( version_compare( $version, '1.0', '<' ) ) { 32 * add_action( 'admin_notices', 'my_plugin_warn_optimization_plugin_outdated' ); 33 * return; 34 * } 35 * 36 * // Bootstrap the Optimization Detective extension. 37 * require_once __DIR__ . '/functions.php'; 38 * // ... 39 * } ); 40 * 25 41 * @since 0.7.0 26 42 * … … 38 54 * @since 0.7.0 39 55 * 40 * @param int |null $minimum_viewport_width Minimum viewport width.41 * @param int |null $maximum_viewport_width Maximum viewport width.56 * @param int<0, max>|null $minimum_viewport_width Minimum viewport width (exclusive). 57 * @param int<1, max>|null $maximum_viewport_width Maximum viewport width (inclusive). 42 58 * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid. 43 59 */ 44 60 function od_generate_media_query( ?int $minimum_viewport_width, ?int $maximum_viewport_width ): ?string { 45 if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width > $maximum_viewport_width ) {46 _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' );61 if ( is_int( $minimum_viewport_width ) && is_int( $maximum_viewport_width ) && $minimum_viewport_width >= $maximum_viewport_width ) { 62 _doing_it_wrong( __FUNCTION__, esc_html__( 'The minimum width cannot be greater than or equal to the maximum width.', 'optimization-detective' ), 'Optimization Detective 0.7.0' ); 47 63 return null; 48 64 } 49 $media_attributes = array(); 50 if ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 ) { 51 $media_attributes[] = sprintf( '(min-width: %dpx)', $minimum_viewport_width ); 52 } 53 if ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ) { 54 $media_attributes[] = sprintf( '(max-width: %dpx)', $maximum_viewport_width ); 55 } 56 if ( count( $media_attributes ) === 0 ) { 65 $has_min_width = ( null !== $minimum_viewport_width && $minimum_viewport_width > 0 ); 66 $has_max_width = ( null !== $maximum_viewport_width && PHP_INT_MAX !== $maximum_viewport_width ); // Note: The use of PHP_INT_MAX is obsolete. 67 if ( $has_min_width && $has_max_width ) { 68 return sprintf( '(%dpx < width <= %dpx)', $minimum_viewport_width, $maximum_viewport_width ); 69 } elseif ( $has_min_width ) { 70 return sprintf( '(%dpx < width)', $minimum_viewport_width ); 71 } elseif ( $has_max_width ) { 72 return sprintf( '(width <= %dpx)', $maximum_viewport_width ); 73 } else { 57 74 return null; 58 75 } 59 return join( ' and ', $media_attributes );60 76 } 61 77 -
optimization-detective/trunk/hooks.php
r3229869 r3240307 19 19 add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX ); 20 20 OD_URL_Metrics_Post_Type::add_hooks(); 21 OD_Storage_Lock::add_hooks(); 21 22 add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' ); 22 23 add_action( 'wp_head', 'od_render_generator_meta_tag' ); -
optimization-detective/trunk/load.php
r3229869 r3240307 3 3 * Plugin Name: Optimization Detective 4 4 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective 5 * Description: Provides a n API for leveraging real user metrics to detect optimizations to apply on the frontend to improvepage performance.5 * Description: Provides a framework for leveraging real user metrics to detect optimizations for improving page performance. 6 6 * Requires at least: 6.6 7 7 * Requires PHP: 7.2 8 * Version: 1.0.0-beta 18 * Version: 1.0.0-beta2 9 9 * Author: WordPress Performance Team 10 10 * Author URI: https://make.wordpress.org/performance/ … … 72 72 )( 73 73 'optimization_detective_pending_plugin', 74 '1.0.0-beta 1',74 '1.0.0-beta2', 75 75 static function ( string $version ): void { 76 76 if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) { -
optimization-detective/trunk/optimization.php
r3229869 r3240307 168 168 // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway). 169 169 is_embed() || 170 // Skip posts that aren't published yet. 171 is_preview() || 170 172 // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context. 171 173 is_customize_preview() || … … 252 254 * Fires to register tag visitors before walking over the document to perform optimizations. 253 255 * 256 * Once a page has finished rendering and the output buffer is processed, the page contents are loaded into 257 * an HTML Tag Processor instance. It then iterates over each tag in the document, and at each open tag it will 258 * invoke all registered tag visitors. A tag visitor is simply a callable (such as a regular function, closure, 259 * or even a class with an `__invoke` method defined). The tag visitor callback is invoked by passing an instance 260 * of the `OD_Tag_Visitor_Context` object which includes the following read-only properties: 261 * 262 * - `$processor` (`OD_HTML_Tag_Processor`): The processor with the cursor at the current open tag. 263 * - `$url_metric_group_collection` (`OD_URL_Metric_Group_Collection`): The URL Metrics which may include information about the current tag to inform what optimizations the callback performs. 264 * - `$link_collection` (`OD_Link_Collection`): Collection of links which will be added to the `HEAD` when the page is served. This allows you to add preload links and preconnect links as needed. 265 * - `$url_metrics_id` (`positive-int|null`): The post ID for the `od_url_metrics` post from which the URL Metrics were loaded (if any). For advanced usage. 266 * 267 * Note that you are free to call `$processor->next_tag()` in the callback (such as to walk over any child elements) 268 * since the tag processor's cursor will be reset to the tag after the callback finishes. 269 * 270 * When a tag visitor sees it is at a relevant open tag (e.g. by checking `$processor->get_tag()`), it can call the 271 * `$context->track_tag()` method to indicate that the tag should be measured during detection. This will cause the 272 * tag to be included among the `elements` in the stored URL Metrics. The element data includes properties such 273 * as `intersectionRatio`, `intersectionRect`, and `boundingClientRect` (provided by an `IntersectionObserver`) as 274 * well as whether the tag is the LCP element (`isLCP`) or LCP element candidate (`isLCPCandidate`). This method 275 * should not be called if the current tag is not relevant for the tag visitor or if the tag visitor callback does 276 * not need to query the provided `OD_URL_Metric_Group_Collection` instance to apply the desired optimizations. (In 277 * addition to calling the `$context->track_tag()`, a callback may also return `true` to indicate the tag should be 278 * tracked.) 279 * 280 * Here's an example tag visitor that depends on URL Metrics data: 281 * 282 * $tag_visitor_registry->register( 283 * 'lcp-img-fetchpriority-high', 284 * static function ( OD_Tag_Visitor_Context $context ): void { 285 * if ( $context->processor->get_tag() !== 'IMG' ) { 286 * return; // Tag is not relevant for this tag visitor. 287 * } 288 * 289 * // Mark the tag for measurement during detection so it is included among the elements stored in URL Metrics. 290 * $context->track_tag(); 291 * 292 * // Make sure fetchpriority=high is added to LCP IMG elements based on the captured URL Metrics. 293 * $common_lcp_element = $context->url_metric_group_collection->get_common_lcp_element(); 294 * if ( 295 * null !== $common_lcp_element 296 * && 297 * $common_lcp_element->get_xpath() === $context->processor->get_xpath() 298 * ) { 299 * $context->processor->set_attribute( 'fetchpriority', 'high' ); 300 * } 301 * } 302 * ); 303 * 304 * Please note this implementation of setting `fetchpriority=high` on the LCP `IMG` element is simplified. Please 305 * see the Image Prioritizer extension for a more robust implementation. 306 * 307 * Here's an example tag visitor that does not depend on any URL Metrics data: 308 * 309 * $tag_visitor_registry->register( 310 * 'img-decoding-async', 311 * static function ( OD_Tag_Visitor_Context $context ): bool { 312 * if ( $context->processor->get_tag() !== 'IMG' ) { 313 * return; // Tag is not relevant for this tag visitor. 314 * } 315 * 316 * // Set the decoding attribute if it is absent. 317 * if ( null === $context->processor->get_attribute( 'decoding' ) ) { 318 * $context->processor->set_attribute( 'decoding', 'async' ); 319 * } 320 * } 321 * ); 322 * 323 * Refer to [Image Prioritizer](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer) and 324 * [Embed Optimizer](https://github.com/WordPress/performance/tree/trunk/plugins/embed-optimizer) for additional 325 * examples of how tag visitors are used. 326 * 254 327 * @since 0.3.0 255 328 * … … 268 341 $link_collection = new OD_Link_Collection(); 269 342 $visited_tag_state = new OD_Visited_Tag_State(); 270 $tag_visitor_context = new OD_Tag_Visitor_Context( $processor, $group_collection, $link_collection, $visited_tag_state ); 343 $tag_visitor_context = new OD_Tag_Visitor_Context( 344 $processor, 345 $group_collection, 346 $link_collection, 347 $visited_tag_state, 348 $post instanceof WP_Post && $post->ID > 0 ? $post->ID : null 349 ); 271 350 $current_tag_bookmark = 'optimization_detective_current_tag'; 272 351 $visitors = iterator_to_array( $tag_visitor_registry ); -
optimization-detective/trunk/readme.txt
r3229869 r3240307 3 3 Contributors: wordpressdotorg 4 4 Tested up to: 6.7 5 Stable tag: 1.0.0-beta 15 Stable tag: 1.0.0-beta2 6 6 License: GPLv2 or later 7 7 License URI: https://www.gnu.org/licenses/gpl-2.0.html 8 8 Tags: performance, optimization, rum 9 9 10 Provides a n API for leveraging real user metrics to detect optimizations to apply on the frontend to improvepage performance.10 Provides a framework for leveraging real user metrics to detect optimizations for improving page performance. 11 11 12 12 == Description == … … 55 55 56 56 == Changelog == 57 58 = 1.0.0-beta2 = 59 60 **Enhancements** 61 62 * Account for 64 KiB limit for sending beacon data. ([1851](https://github.com/WordPress/performance/pull/1851)) 63 * Add post ID for the `od_url_metrics` post to the tag visitor context. ([1847](https://github.com/WordPress/performance/pull/1847)) 64 * Change minimum viewport width to be exclusive whereas the maximum width remains inclusive. ([1839](https://github.com/WordPress/performance/pull/1839)) 65 * Disable URL Metric storage locking by default for administrators. ([1835](https://github.com/WordPress/performance/pull/1835)) 66 * Include active plugins in ETag data and increase default freshness TTL from 1 day to 1 week. ([1854](https://github.com/WordPress/performance/pull/1854)) 67 * Make ETag a required property of the URL Metric. ([1824](https://github.com/WordPress/performance/pull/1824)) 68 * Use CSS range syntax in media queries. ([1833](https://github.com/WordPress/performance/pull/1833)) 69 * Use `IFRAME` to display HTML responses for REST API storage request failures in Site Health test. ([1849](https://github.com/WordPress/performance/pull/1849)) 70 71 **Bug Fixes** 72 73 * Prevent URL in `Link` header from including invalid characters. ([1802](https://github.com/WordPress/performance/pull/1802)) 74 * Prevent optimizing post previews by default. ([1848](https://github.com/WordPress/performance/pull/1848)) 75 76 **Documentation** 77 78 * Improve Optimization Detective documentation. ([1782](https://github.com/WordPress/performance/pull/1782)) 57 79 58 80 = 1.0.0-beta1 = -
optimization-detective/trunk/site-health.php
r3229869 r3240307 128 128 $body = wp_remote_retrieve_body( $response ); 129 129 $data = json_decode( $body, true ); 130 $header = wp_remote_retrieve_header( $response, 'content-type' ); 131 if ( is_array( $header ) ) { 132 $header = array_pop( $header ); 133 } 130 134 131 135 $is_expected = ( … … 157 161 } 158 162 159 $result['description'] .= '<details><summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary><pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre></details>'; 163 if ( '' !== $body ) { 164 $result['description'] .= '<details>'; 165 $result['description'] .= '<summary>' . esc_html__( 'Raw response:', 'optimization-detective' ) . '</summary>'; 166 167 if ( is_string( $header ) && str_contains( $header, 'html' ) ) { 168 $escaped_content = htmlspecialchars( $body, ENT_QUOTES, 'UTF-8' ); 169 $result['description'] .= '<iframe srcdoc="' . $escaped_content . '" sandbox width="100%" height="300"></iframe>'; 170 } else { 171 $result['description'] .= '<pre style="white-space: pre-wrap">' . esc_html( $body ) . '</pre>'; 172 } 173 $result['description'] .= '</details>'; 174 } 160 175 } 161 176 } … … 239 254 } 240 255 241 wp_admin_notice(256 $notice = wp_get_admin_notice( 242 257 $message, 243 258 array( … … 247 262 ) 248 263 ); 264 265 echo wp_kses( 266 $notice, 267 array_merge( 268 wp_kses_allowed_html( 'post' ), 269 array( 270 'iframe' => array_fill_keys( array( 'srcdoc', 'sandbox', 'width', 'height' ), true ), 271 ) 272 ) 273 ); 249 274 } 250 275 … … 255 280 * @access private 256 281 * 257 * @param string $plugin_file Plugin file.282 * @param non-empty-string $plugin_file Plugin file. 258 283 */ 259 284 function od_render_rest_api_health_check_admin_notice_in_plugin_row( string $plugin_file ): void { -
optimization-detective/trunk/storage/class-od-storage-lock.php
r3229869 r3240307 22 22 23 23 /** 24 * Capability for being able to store a URL Metric now. 25 * 26 * @since 1.0.0 27 * @var string 28 */ 29 const STORE_URL_METRIC_NOW_CAPABILITY = 'od_store_url_metric_now'; 30 31 /** 32 * Adds hooks. 33 * 34 * @since 1.0.0 35 */ 36 public static function add_hooks(): void { 37 add_filter( 'user_has_cap', array( __CLASS__, 'filter_user_has_cap' ) ); 38 } 39 40 /** 41 * Filters `user_has_cap` to grant the `od_store_url_metric_now` capability to users who can `manage_options` by default. 42 * 43 * @since 1.0.0 44 * 45 * @param array<string, bool>|mixed $allcaps Capability names mapped to boolean values for whether the user has that capability. 46 * @return array<string, bool> Capability names mapped to boolean values for whether the user has that capability. 47 */ 48 public static function filter_user_has_cap( $allcaps ): array { 49 if ( ! is_array( $allcaps ) ) { 50 $allcaps = array(); 51 } 52 if ( isset( $allcaps['manage_options'] ) ) { 53 $allcaps['od_store_url_metric_now'] = $allcaps['manage_options']; 54 } 55 return $allcaps; 56 } 57 58 /** 24 59 * Gets the TTL (in seconds) for the URL Metric storage lock. 25 60 * 26 61 * @since 0.1.0 27 * @access private28 62 * 29 * @return int TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used.63 * @return int<0, max> TTL in seconds, greater than or equal to zero. A value of zero means that the storage lock should be disabled and thus that transients must not be used. 30 64 */ 31 65 public static function get_ttl(): int { 66 $ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS; 32 67 33 68 /** 34 * Filters how long a given IP is locked from submitting another metric-storage REST API request.69 * Filters how long the current IP is locked from submitting another URL metric storage REST API request. 35 70 * 36 * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable71 * Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable 37 72 * locking when a user is logged-in with code like the following: 38 73 * … … 41 76 * } ); 42 77 * 78 * By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current 79 * user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This 80 * custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter. 81 * 43 82 * @since 0.1.0 83 * @since 1.0.0 This now defaults to zero (0) for authorized users. 44 84 * 45 * @param int $ttl TTL. 85 * @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users. 46 86 */ 47 $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS);87 $ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', $ttl ); 48 88 return max( 0, $ttl ); 49 89 } … … 52 92 * Gets transient key for locking URL Metric storage (for the current IP). 53 93 * 54 * @todo Should the URL be included in the key? Or should a user only be allowed to store one metric? 55 * @return string Transient key. 94 * @since 0.1.0 95 * 96 * @return non-empty-string Transient key. 56 97 */ 57 98 public static function get_transient_key(): string { … … 67 108 * 68 109 * @since 0.1.0 69 * @access private70 110 */ 71 111 public static function set_lock(): void { … … 83 123 * 84 124 * @since 0.1.0 85 * @access private86 125 * 87 126 * @return bool Whether locked. -
optimization-detective/trunk/storage/class-od-url-metric-store-request-context.php
r3229869 r3240307 17 17 * 18 18 * @since 0.7.0 19 * @access private 19 * 20 * @property-read WP_REST_Request<array<string, mixed>> $request Request. 21 * @property-read positive-int $url_metrics_id ID for the od_url_metrics post. 22 * @property-read OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 23 * @property-read OD_URL_Metric_Group $url_metric_group URL Metric group. 24 * @property-read OD_URL_Metric $url_metric URL Metric. 25 * @property-read positive-int $post_id Deprecated alias for the $url_metrics_id property. 20 26 */ 21 27 final class OD_URL_Metric_Store_Request_Context { … … 24 30 * Request. 25 31 * 32 * @since 0.7.0 26 33 * @var WP_REST_Request<array<string, mixed>> 27 * @readonly28 34 */ 29 p ublic$request;35 private $request; 30 36 31 37 /** 32 * ID for the URL Metricpost.38 * ID for the od_url_metrics post. 33 39 * 34 * @var int 35 * @readonly 40 * This was originally $post_id which was introduced in 0.7.0. 41 * 42 * @since 1.0.0 43 * @var positive-int 36 44 */ 37 p ublic $post_id;45 private $url_metrics_id; 38 46 39 47 /** 40 48 * URL Metric group collection. 41 49 * 50 * @since 0.7.0 42 51 * @var OD_URL_Metric_Group_Collection 43 * @readonly44 52 */ 45 p ublic$url_metric_group_collection;53 private $url_metric_group_collection; 46 54 47 55 /** 48 56 * URL Metric group. 49 57 * 58 * @since 0.7.0 50 59 * @var OD_URL_Metric_Group 51 * @readonly52 60 */ 53 p ublic$url_metric_group;61 private $url_metric_group; 54 62 55 63 /** 56 64 * URL Metric. 57 65 * 66 * @since 0.7.0 58 67 * @var OD_URL_Metric 59 * @readonly60 68 */ 61 p ublic$url_metric;69 private $url_metric; 62 70 63 71 /** 64 72 * Constructor. 65 73 * 74 * @since 0.7.0 75 * 66 76 * @phpstan-param WP_REST_Request<array<string, mixed>> $request 67 77 * 68 78 * @param WP_REST_Request $request REST API request. 69 * @param int $post_idID for the URL Metric post.79 * @param positive-int $url_metrics_id ID for the URL Metric post. 70 80 * @param OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection. 71 81 * @param OD_URL_Metric_Group $url_metric_group URL Metric group. 72 82 * @param OD_URL_Metric $url_metric URL Metric. 73 83 */ 74 public function __construct( WP_REST_Request $request, int $ post_id, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_URL_Metric_Group $url_metric_group, OD_URL_Metric $url_metric ) {84 public function __construct( WP_REST_Request $request, int $url_metrics_id, OD_URL_Metric_Group_Collection $url_metric_group_collection, OD_URL_Metric_Group $url_metric_group, OD_URL_Metric $url_metric ) { 75 85 $this->request = $request; 76 $this-> post_id = $post_id;86 $this->url_metrics_id = $url_metrics_id; 77 87 $this->url_metric_group_collection = $url_metric_group_collection; 78 88 $this->url_metric_group = $url_metric_group; 79 89 $this->url_metric = $url_metric; 80 90 } 91 92 /** 93 * Gets a property. 94 * 95 * @since 1.0.0 96 * 97 * @param string $name Property name. 98 * @return mixed Property value. 99 * 100 * @throws Error When property is unknown. 101 */ 102 public function __get( string $name ) { 103 switch ( $name ) { 104 case 'request': 105 return $this->request; 106 case 'url_metrics_id': 107 return $this->url_metrics_id; 108 case 'url_metric_group_collection': 109 return $this->url_metric_group_collection; 110 case 'url_metric_group': 111 return $this->url_metric_group; 112 case 'url_metric': 113 return $this->url_metric; 114 case 'post_id': 115 _doing_it_wrong( 116 esc_html( __CLASS__ . '::$' . $name ), 117 esc_html( 118 sprintf( 119 /* translators: %s is class member variable name */ 120 __( 'Use %s instead.', 'optimization-detective' ), 121 __CLASS__ . '::$url_metrics_id' 122 ) 123 ), 124 'optimization-detective 1.0.0' 125 ); 126 return $this->url_metrics_id; 127 default: 128 throw new Error( 129 esc_html( 130 sprintf( 131 /* translators: %s is class member variable name */ 132 __( 'Unknown property %s.', 'optimization-detective' ), 133 __CLASS__ . '::$' . $name 134 ) 135 ) 136 ); 137 } 138 } 81 139 } -
optimization-detective/trunk/storage/class-od-url-metrics-post-type.php
r3229869 r3240307 24 24 * Post type slug. 25 25 * 26 * @since 0.1.0 26 27 * @var string 27 28 */ … … 31 32 * Event name (hook) for garbage collection of stale URL Metrics posts. 32 33 * 34 * @since 0.1.0 33 35 * @var string 34 36 */ … … 38 40 * Recurrence for garbage collection of stale URL Metrics posts. 39 41 * 42 * @since 0.1.0 40 43 * @var string 41 44 */ … … 85 88 * @since 0.1.0 86 89 * 87 * @param string $slug URL Metrics slug.90 * @param non-empty-string $slug URL Metrics slug. 88 91 * @return WP_Post|null Post object if exists. 89 92 */ … … 203 206 * @todo There is duplicate logic here with od_handle_rest_request(). 204 207 * 205 * @param string $slugSlug (hash of normalized query vars).206 * @param OD_URL_Metric $new_url_metric New URL Metric.207 * @return int|WP_Error Post ID or WP_Error otherwise.208 * @param non-empty-string $slug Slug (hash of normalized query vars). 209 * @param OD_URL_Metric $new_url_metric New URL Metric. 210 * @return positive-int|WP_Error Post ID or WP_Error otherwise. 208 211 */ 209 212 public static function store_url_metric( string $slug, OD_URL_Metric $new_url_metric ) { … … 227 230 228 231 $etag = $new_url_metric->get_etag(); 229 if ( null === $etag ) {230 // This case actually will never occur in practice because the store_url_metric function is only called231 // in the REST API endpoint where the ETag parameter is required. It is here exclusively for the sake of232 // PHPStan's static analysis. This entire condition can be removed in a future release when the 'etag'233 // property becomes required.234 return new WP_Error( 'missing_etag' );235 }236 232 237 233 $group_collection = new OD_URL_Metric_Group_Collection( … … 251 247 252 248 $post_data['post_content'] = wp_json_encode( 253 array_map( 254 static function ( OD_URL_Metric $url_metric ): array { 255 return $url_metric->jsonSerialize(); 256 }, 257 $group_collection->get_flattened_url_metrics() 258 ), 259 JSON_UNESCAPED_SLASHES // No need for escaped slashes since not printed to frontend. 249 $group_collection->get_flattened_url_metrics(), 250 JSON_UNESCAPED_SLASHES // No need for escaping slashes since this JSON is not embedded in HTML. 260 251 ); 261 252 if ( ! is_string( $post_data['post_content'] ) ) { -
optimization-detective/trunk/storage/data.php
r3229869 r3240307 21 21 * @access private 22 22 * 23 * @return int Expiration TTL in seconds.23 * @return int<0, max> Expiration TTL in seconds. 24 24 */ 25 25 function od_get_url_metric_freshness_ttl(): int { … … 32 32 * @since 0.1.0 33 33 * 34 * @param int $ttl Expiration TTL in seconds. Defaults to 1 day.35 */ 36 $freshness_ttl = (int) apply_filters( 'od_url_metric_freshness_ttl', DAY_IN_SECONDS );34 * @param int $ttl Expiration TTL in seconds. Defaults to 1 week. 35 */ 36 $freshness_ttl = (int) apply_filters( 'od_url_metric_freshness_ttl', WEEK_IN_SECONDS ); 37 37 38 38 if ( $freshness_ttl < 0 ) { … … 59 59 * This is used as a cache key for stored URL Metrics. 60 60 * 61 * TODO: For non-singular requests, consider adding the post IDs from The Loop to ensure publishing a new post will invalidate the cache.62 *63 61 * @since 0.1.0 64 62 * @access private … … 118 116 $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' ); 119 117 } 118 119 // TODO: We should be able to assert that this returns an non-empty-string. 120 120 return esc_url_raw( $current_url ); 121 121 } … … 132 132 * 133 133 * @param array<string, mixed> $query_vars Normalized query vars. 134 * @return string Slug.134 * @return non-empty-string Slug. 135 135 */ 136 136 function od_get_url_metrics_slug( array $query_vars ): string { … … 199 199 $queried_object_data['type'] = $queried_object->name; 200 200 } 201 202 $active_plugins = (array) get_option( 'active_plugins', array() ); 203 if ( is_multisite() ) { 204 $active_plugins = array_unique( 205 array_merge( 206 $active_plugins, 207 array_keys( (array) get_site_option( 'active_sitewide_plugins', array() ) ) 208 ) 209 ); 210 } 211 sort( $active_plugins ); 201 212 202 213 $data = array( … … 231 242 ), 232 243 ), 244 'active_plugins' => $active_plugins, 233 245 'current_template' => $current_template instanceof WP_Block_Template ? get_object_vars( $current_template ) : $current_template, 234 246 ); … … 258 270 * @see od_get_url_metrics_slug() 259 271 * 260 * @param string$slug Slug (hash of normalized query vars).261 * @param non-empty-string $current_etag Current ETag.262 * @param string $url URL.263 * @param int|null$cache_purge_post_id Cache purge post ID.264 * @return string HMAC.272 * @param non-empty-string $slug Slug (hash of normalized query vars). 273 * @param non-empty-string $current_etag Current ETag. 274 * @param string $url URL. 275 * @param positive-int|null $cache_purge_post_id Cache purge post ID. 276 * @return non-empty-string HMAC. 265 277 */ 266 278 function od_get_url_metrics_storage_hmac( string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): string { 267 279 $action = "store_url_metric:$slug:$current_etag:$url:$cache_purge_post_id"; 268 return wp_hash( $action, 'nonce' ); 280 281 /** 282 * HMAC. 283 * 284 * @var non-empty-string $hmac 285 */ 286 $hmac = wp_hash( $action, 'nonce' ); 287 return $hmac; 269 288 } 270 289 … … 279 298 * @see od_get_url_metrics_slug() 280 299 * 281 * @param string$hmac HMAC.282 * @param string$slug Slug (hash of normalized query vars).283 * @param non-empty-string $current_etag Current ETag.284 * @param string $url URL.285 * @param int|null$cache_purge_post_id Cache purge post ID.300 * @param non-empty-string $hmac HMAC. 301 * @param non-empty-string $slug Slug (hash of normalized query vars). 302 * @param non-empty-string $current_etag Current ETag. 303 * @param string $url URL. 304 * @param positive-int|null $cache_purge_post_id Cache purge post ID. 286 305 * @return bool Whether the HMAC is valid. 287 306 */ … … 361 380 * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13 362 381 * 363 * @return int[] Breakpoint max widths, sorted in ascending order.382 * @return positive-int[] Breakpoint max widths, sorted in ascending order. 364 383 */ 365 384 function od_get_breakpoint_max_widths(): array { … … 369 388 static function ( $original_breakpoint ) use ( $function_name ): int { 370 389 $breakpoint = $original_breakpoint; 371 if ( PHP_INT_MAX === $breakpoint ) { 372 $breakpoint = PHP_INT_MAX - 1; 373 _doing_it_wrong( 374 esc_html( $function_name ), 375 esc_html( 376 sprintf( 377 /* translators: %s is the actual breakpoint max width */ 378 __( 'Breakpoint must be less than PHP_INT_MAX, but saw "%s".', 'optimization-detective' ), 379 $original_breakpoint 380 ) 381 ), 382 '' 383 ); 384 } elseif ( $breakpoint <= 0 ) { 390 if ( $breakpoint <= 0 ) { 385 391 $breakpoint = 1; 386 392 _doing_it_wrong( … … 401 407 * Filters the breakpoint max widths to group URL Metrics for various viewports. 402 408 * 403 * A breakpoint must be greater than zero and less than PHP_INT_MAX. This array may be empty in which case there409 * A breakpoint must be greater than zero. This array may be empty in which case there 404 410 * are no responsive breakpoints and all URL Metrics are collected in a single group. 405 411 * 406 412 * @since 0.1.0 407 413 * 408 * @param int[] $breakpoint_max_widths Max widths for viewport breakpoints. Defaults to [480, 600, 782].414 * @param positive-int[] $breakpoint_max_widths Max widths for viewport breakpoints. Defaults to [480, 600, 782]. 409 415 */ 410 416 array_map( 'intval', (array) apply_filters( 'od_breakpoint_max_widths', array( 480, 600, 782 ) ) ) … … 426 432 * @access private 427 433 * 428 * @return int Sample size.434 * @return int<1, max> Sample size. 429 435 */ 430 436 function od_get_url_metrics_breakpoint_sample_size(): int { -
optimization-detective/trunk/storage/rest-api.php
r3229869 r3240307 16 16 * Namespace for optimization-detective. 17 17 * 18 * @since 0.1.0 19 * @access private 18 20 * @var string 19 21 */ … … 27 29 * create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug. 28 30 * 31 * @since 0.1.0 32 * @access private 29 33 * @link https://google.aip.dev/136 30 34 * @var string … … 73 77 'pattern' => '^[0-9a-f]+\z', 74 78 'validate_callback' => static function ( string $hmac, WP_REST_Request $request ) { 75 if ( ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['current_etag'], $request['url'], $request['cache_purge_post_id'] ?? null ) ) {79 if ( '' === $hmac || ! od_verify_url_metrics_storage_hmac( $hmac, $request['slug'], $request['current_etag'], $request['url'], $request['cache_purge_post_id'] ?? null ) ) { 76 80 return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) ); 77 81 } … … 215 219 } 216 220 221 /* 222 * The limit for data sent via navigator.sendBeacon() is 64 KiB. This limit is checked in detect.js so that the 223 * request will not even be attempted if the payload is too large. This server-side restriction is added as a 224 * safeguard against clients sending possibly malicious payloads much larger than 64 KiB which should never be 225 * getting sent. 226 */ 227 $max_size = 64 * 1024; 228 $content_length = strlen( (string) wp_json_encode( $url_metric ) ); 229 if ( $content_length > $max_size ) { 230 return new WP_Error( 231 'rest_content_too_large', 232 sprintf( 233 /* translators: 1: the size of the payload, 2: the maximum allowed payload size */ 234 __( 'JSON payload size is %1$s bytes which is larger than the maximum allowed size of %2$s bytes.', 'optimization-detective' ), 235 number_format_i18n( $content_length ), 236 number_format_i18n( $max_size ) 237 ), 238 array( 'status' => 413 ) 239 ); 240 } 241 217 242 // TODO: This should be changed from store_url_metric($slug, $url_metric) instead be update_post( $slug, $group_collection ). As it stands, store_url_metric() is duplicating logic here. 218 243 $result = OD_URL_Metrics_Post_Type::store_url_metric( -
optimization-detective/trunk/types.ts
r3229869 r3240307 35 35 export interface URLMetricGroupStatus { 36 36 minimumViewportWidth: number; 37 maximumViewportWidth: number | null; 37 38 complete: boolean; 38 39 }
Note: See TracChangeset
for help on using the changeset viewer.