Plugin Directory

Changeset 3240307


Ignore:
Timestamp:
02/13/2025 07:42:05 PM (9 months ago)
Author:
performanceteam
Message:

Update to version 1.0.0-beta2 from GitHub

Location:
optimization-detective
Files:
54 edited
1 copied

Legend:

Unmodified
Added
Removed
  • optimization-detective/tags/1.0.0-beta2/class-od-data-validation-exception.php

    r3229869 r3240307  
    1717 *
    1818 * @since 0.1.0
    19  * @access private
    2019 */
    2120final class OD_Data_Validation_Exception extends Exception {}
  • optimization-detective/tags/1.0.0-beta2/class-od-element.php

    r3229869 r3240307  
    2222 *
    2323 * @since 0.7.0
    24  * @access private
    2524 */
    2625class OD_Element implements ArrayAccess, JsonSerializable {
  • optimization-detective/tags/1.0.0-beta2/class-od-html-tag-processor.php

    r3229869 r3240307  
    1717 *
    1818 * @since 0.1.1
    19  * @access private
    2019 */
    2120final class OD_HTML_Tag_Processor extends WP_HTML_Tag_Processor {
     
    150149     *
    151150     * @since 0.4.0
    152      * @var string[]
     151     * @var non-empty-string[]
    153152     */
    154153    private $open_stack_tags = array();
     
    161160     *
    162161     * @since 1.0.0
    163      * @var array<array<string, string>>
     162     * @var array<array<non-empty-string, string>>
    164163     */
    165164    private $open_stack_attributes = array();
     
    169168     *
    170169     * @since 0.4.0
    171      * @var int[]
     170     * @var non-negative-int[]
    172171     */
    173172    private $open_stack_indices = array();
     
    182181     *
    183182     * @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[]}>
    185184     */
    186185    private $bookmarked_open_stacks = array();
     
    222221     *
    223222     * @since 0.4.0
    224      * @var array<string, string[]>
     223     * @var array<non-empty-string, string[]>
    225224     */
    226225    private $buffered_text_replacements = array();
     
    239238     *
    240239     * @since 0.6.0
    241      * @var int
     240     * @var non-negative-int
    242241     * @see self::next_token()
    243242     * @see self::seek()
     
    293292     * @since 0.4.0
    294293     *
    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.
    296295     * @return bool Whether to expect a closer for the tag.
    297296     */
     
    337336            return true;
    338337        }
     338        /**
     339         * Tag name.
     340         *
     341         * @var non-empty-string $tag_name
     342         */
    339343
    340344        if ( $this->previous_tag_without_closer ) {
     
    423427     * @see self::seek()
    424428     *
    425      * @return int Count of times the cursor has moved.
     429     * @return non-negative-int Count of times the cursor has moved.
    426430     */
    427431    public function get_cursor_move_count(): int {
     
    459463     * @since 0.4.0
    460464     *
    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.
    463467     * @return bool Whether an attribute was set.
    464468     */
     
    490494     * @see WP_HTML_Processor::get_current_depth()
    491495     *
    492      * @return int Nesting-depth of current location in the document.
     496     * @return non-negative-int Nesting-depth of current location in the document.
    493497     */
    494498    public function get_current_depth(): int {
     
    566570     * @since 0.9.0 Renamed from get_breadcrumbs() to get_indexed_breadcrumbs().
    567571     *
    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.
    569573     */
    570574    private function get_indexed_breadcrumbs(): Generator {
     
    585589     * @since 1.0.0
    586590     *
    587      * @return array<string, string> Disambiguating attributes.
     591     * @return array<non-empty-string, string> Disambiguating attributes.
    588592     */
    589593    private function get_disambiguating_attributes(): array {
     
    618622     * @see WP_HTML_Processor::get_breadcrumbs()
    619623     *
    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.
    621625     */
    622626    public function get_breadcrumbs(): array {
  • optimization-detective/tags/1.0.0-beta2/class-od-link-collection.php

    r3229869 r3240307  
    1919 *                   attributes: LinkAttributes,
    2020 *                   minimum_viewport_width: int<0, max>|null,
    21  *                   maximum_viewport_width: positive-int|null
     21 *                   maximum_viewport_width: int<1, max>|null
    2222 *               }
    2323 *
     
    3838 * @since 0.3.0
    3939 * @since 0.4.0 Renamed from OD_Preload_Link_Collection.
    40  * @access private
    4140 */
    4241final class OD_Link_Collection implements Countable {
     
    4544     * Links grouped by rel type.
    4645     *
     46     * @since 0.4.0
     47     *
    4748     * @var array<string, Link[]>
    4849     */
     
    5253     * Adds link.
    5354     *
     55     * @since 0.3.0
     56     *
    5457     * @phpstan-param LinkAttributes $attributes
    5558     *
    56      * @param array             $attributes             Attributes.
    57      * @param int<0, max>|null  $minimum_viewport_width Minimum width or null if not bounded or relevant.
    58      * @param positive-int|null $maximum_viewport_width Maximum width or 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.
    5962     *
    6063     * @throws InvalidArgumentException When invalid arguments are provided.
     
    112115     * together. Also, add media attributes to the links.
    113116     *
     117     * @since 0.4.0
     118     *
    114119     * @return LinkAttributes[] Prepared links with adjacent-duplicates merged together and media attributes added.
    115120     */
     
    133138    /**
    134139     * Merges consecutive links.
     140     *
     141     * @since 0.4.0
    135142     *
    136143     * @param Link[] $links Links.
     
    195202                    is_int( $last_link['maximum_viewport_width'] )
    196203                    &&
    197                     $last_link['maximum_viewport_width'] + 1 === $link['minimum_viewport_width']
     204                    $last_link['maximum_viewport_width'] === $link['minimum_viewport_width']
    198205                ) {
    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'] );
    200207
    201208                    // Update the last link with the new maximum viewport width.
     
    229236     * Gets the HTML for the link tags.
    230237     *
     238     * @since 0.3.0
     239     *
    231240     * @return string Link tags HTML.
    232241     */
     
    250259     * Constructs the Link HTTP response header.
    251260     *
    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.
    253264     */
    254265    public function get_response_header(): ?string {
     
    256267
    257268        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'] . '>';
    261297            unset( $link['href'] );
    262298            foreach ( $link as $name => $value ) {
     
    286322
    287323    /**
     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    /**
    288346     * Counts the links.
     347     *
     348     * @since 0.3.0
    289349     *
    290350     * @return non-negative-int Link count.
  • optimization-detective/tags/1.0.0-beta2/class-od-tag-visitor-context.php

    r3229869 r3240307  
    1818 * @since 0.4.0
    1919 *
    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.
    2125 */
    2226final class OD_Tag_Visitor_Context {
     
    2731     * @since 0.4.0
    2832     * @var OD_HTML_Tag_Processor
    29      * @readonly
    3033     */
    31     public $processor;
     34    private $processor;
    3235
    3336    /**
     
    3639     * @since 0.4.0
    3740     * @var OD_URL_Metric_Group_Collection
    38      * @readonly
    3941     */
    40     public $url_metric_group_collection;
     42    private $url_metric_group_collection;
    4143
    4244    /**
     
    4547     * @since 0.4.0
    4648     * @var OD_Link_Collection
    47      * @readonly
    4849     */
    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;
    5061
    5162    /**
    5263     * Visited tag state.
     64     *
     65     * Important: This object is not exposed directly by the getter. It is only exposed via {@see self::track_tag()}.
    5366     *
    5467     * @since 1.0.0
     
    6679     * @param OD_Link_Collection             $link_collection             Link collection.
    6780     * @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.
    6882     */
    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 ) {
    7084        $this->processor                   = $processor;
    7185        $this->url_metric_group_collection = $url_metric_group_collection;
    7286        $this->link_collection             = $link_collection;
    7387        $this->visited_tag_state           = $visited_tag_state;
     88        $this->url_metrics_id              = $url_metrics_id;
    7489    }
    7590
     
    86101
    87102    /**
    88      * Gets deprecated property.
     103     * Gets a property.
    89104     *
    90105     * @since 0.7.0
    91      * @todo Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore.
    92106     *
    93107     * @param string $name Property name.
    94      * @return OD_URL_Metric_Group_Collection URL Metric group collection.
     108     * @return mixed Property value.
    95109     *
    96110     * @throws Error When property is unknown.
    97111     */
    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                        )
    107145                    )
    108                 ),
    109                 'optimization-detective 0.7.0'
    110             );
    111             return $this->url_metric_group_collection;
     146                );
    112147        }
    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__ . '::$' . $name
    119                 )
    120             )
    121         );
    122148    }
    123149}
  • optimization-detective/tags/1.0.0-beta2/class-od-tag-visitor-registry.php

    r3229869 r3240307  
    2121 *
    2222 * @since 0.3.0
    23  * @access private
    2423 */
    2524final class OD_Tag_Visitor_Registry implements Countable, IteratorAggregate {
     
    2827     * Visitors.
    2928     *
    30      * @var array<string, TagVisitorCallback>
     29     * @since 0.3.0
     30     *
     31     * @var array<non-empty-string, TagVisitorCallback>
    3132     */
    3233    private $visitors = array();
     
    3536     * Registers a tag visitor.
    3637     *
     38     * @since 0.3.0
     39     *
    3740     * @phpstan-param TagVisitorCallback $tag_visitor_callback
    3841     *
    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.
    4144     */
    4245    public function register( string $id, callable $tag_visitor_callback ): void {
     
    4750     * Determines if a visitor has been registered.
    4851     *
    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.
    5055     * @return bool Whether registered.
    5156     */
     
    5762     * Gets a registered visitor.
    5863     *
    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.
    6067     * @return TagVisitorCallback|null Whether registered.
    6168     */
     
    7077     * Unregisters a tag visitor.
    7178     *
    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.
    7382     * @return bool Whether a tag visitor was unregistered.
    7483     */
     
    8493     * Returns an iterator for the URL Metrics in the group.
    8594     *
     95     * @since 0.3.0
     96     *
    8697     * @return ArrayIterator<string, TagVisitorCallback> ArrayIterator for tag visitors.
    8798     */
     
    93104     * Counts the URL Metrics in the group.
    94105     *
     106     * @since 0.3.0
     107     *
    95108     * @return int<0, max> URL Metric count.
    96109     */
  • optimization-detective/tags/1.0.0-beta2/class-od-url-metric-group-collection.php

    r3229869 r3240307  
    1919 *
    2020 * @since 0.1.0
    21  * @access private
    2221 */
    2322final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggregate, JsonSerializable {
     
    3231     * in this case, in which every single URL Metric is added.
    3332     *
     33     * @since 0.1.0
    3434     * @var OD_URL_Metric_Group[]
    3535     * @phpstan-var non-empty-array<OD_URL_Metric_Group>
     
    4848     * Breakpoints in max widths.
    4949     *
    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.
    5653     *
    5754     * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a
    5855     * single group.
    5956     *
    60      * @var int[]
    61      * @phpstan-var positive-int[]
     57     * @since 0.1.0
     58     * @var positive-int[]
    6259     */
    6360    private $breakpoints;
     
    6663     * Sample size for URL Metrics for a given breakpoint.
    6764     *
    68      * @var int
    69      * @phpstan-var positive-int
     65     * @since 0.1.0
     66     * @var int<1, max>
    7067     */
    7168    private $sample_size;
     
    7673     * A freshness age of zero means a URL Metric will always be considered stale.
    7774     *
    78      * @var int
    79      * @phpstan-var 0|positive-int
     75     * @since 0.1.0
     76     * @var int<0, max>
    8077     */
    8178    private $freshness_ttl;
     
    8481     * Result cache.
    8582     *
     83     * @since 0.3.0
    8684     * @var array{
    8785     *          get_group_for_viewport_width?: array<int, OD_URL_Metric_Group>,
     
    9290     *          get_common_lcp_element?: OD_Element|null,
    9391     *          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>>,
    9593     *          get_all_elements_positioned_in_any_initial_viewport?: array<string, bool>,
    9694     *      }
     
    10199     * Constructor.
    102100     *
     101     * @since 0.1.0
     102     *
    103103     * @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
    104108     *
    105109     * @param OD_URL_Metric[]  $url_metrics   URL Metrics.
     
    128132        $breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) );
    129133        foreach ( $breakpoints as $breakpoint ) {
    130             if ( ! is_int( $breakpoint ) || $breakpoint < 1 || PHP_INT_MAX === $breakpoint ) {
     134            if ( ! is_int( $breakpoint ) || $breakpoint < 1 ) {
    131135                throw new InvalidArgumentException(
    132136                    esc_html(
     
    134138                            /* translators: %d is the invalid breakpoint */
    135139                            __(
    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',
    137141                                'optimization-detective'
    138142                            ),
     
    197201
    198202    /**
     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    /**
    199236     * Gets the first URL Metric group.
    200237     *
     
    216253     * This group normally represents viewports for desktop devices.  This group always has a minimum viewport width
    217254     * 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.
    219256     *
    220257     * @since 0.7.0
     
    245282     */
    246283    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 );
    254291        return $groups;
    255292    }
     
    273310        }
    274311        // @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.
    276313        throw new InvalidArgumentException(
    277314            esc_html__( 'No group available to add URL Metric to.', 'optimization-detective' )
     
    286323     * @throws InvalidArgumentException When there is no group for the provided viewport width. This would only happen if a negative width is provided.
    287324     *
    288      * @param int $viewport_width Viewport width.
     325     * @param positive-int $viewport_width Viewport width.
    289326     * @return OD_URL_Metric_Group URL Metric group for the viewport width.
    290327     */
     
    301338            }
    302339            // @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.
    304341            throw new InvalidArgumentException(
    305342                esc_html(
     
    496533     * @since 0.7.0
    497534     *
    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.
    499536     */
    500537    public function get_xpath_elements_map(): array {
     
    668705     *             groups: array<int, array{
    669706     *                 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,
    672709     *                 complete: bool,
    673710     *                 url_metrics: OD_URL_Metric[]
  • optimization-detective/tags/1.0.0-beta2/class-od-url-metric-group.php

    r3229869 r3240307  
    1919 *
    2020 * @since 0.1.0
    21  * @access private
    2221 */
    2322final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSerializable {
     
    3332
    3433    /**
    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>
    4139     */
    4240    private $minimum_viewport_width;
    4341
    4442    /**
    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
    5148     */
    5249    private $maximum_viewport_width;
     
    5754     * @since 0.1.0
    5855     *
    59      * @var int
    60      * @phpstan-var positive-int
     56     * @var int<1, max>
    6157     */
    6258    private $sample_size;
     
    6763     * @since 0.1.0
    6864     *
    69      * @var int
    70      * @phpstan-var 0|positive-int
     65     * @var int<0, max>
    7166     */
    7267    private $freshness_ttl;
     
    10095     * This class should never be directly constructed. It should only be constructed by the {@see OD_URL_Metric_Group_Collection::create_groups()}.
    10196     *
     97     * @since 0.1.0
     98     *
    10299     * @access private
    103100     * @throws InvalidArgumentException If arguments are invalid.
    104101     *
     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     *
    105107     * @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.
    108110     * @param int                            $sample_size            Sample size for the maximum number of viewports in a group between breakpoints.
    109111     * @param int                            $freshness_ttl          Freshness age (TTL) for a given URL Metric.
    110112     * @param OD_URL_Metric_Group_Collection $collection             Collection that this instance belongs to.
    111113     */
    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 ) {
    113115        if ( $minimum_viewport_width < 0 ) {
    114116            throw new InvalidArgumentException(
     
    116118            );
    117119        }
    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            }
    127131        }
    128132        $this->minimum_viewport_width = $minimum_viewport_width;
     
    159163
    160164    /**
    161      * Gets the minimum possible viewport width (inclusive).
     165     * Gets the minimum possible viewport width (exclusive).
    162166     *
    163167     * @since 0.1.0
    164168     *
    165169     * @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).
    167171     */
    168172    public function get_minimum_viewport_width(): int {
     
    176180     *
    177181     * @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 {
    181185        return $this->maximum_viewport_width;
    182186    }
     
    188192     *
    189193     * @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.
    192195     */
    193196    public function get_sample_size(): int {
     
    201204     *
    202205     * @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.
    205207     */
    206208    public function get_freshness_ttl(): int {
     
    209211
    210212    /**
    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
    214230     *
    215231     * @param int $viewport_width Viewport width.
     
    218234    public function is_viewport_width_in_range( int $viewport_width ): bool {
    219235        return (
    220             $viewport_width >= $this->minimum_viewport_width &&
    221             $viewport_width <= $this->maximum_viewport_width
     236            $viewport_width > $this->minimum_viewport_width &&
     237            ( null === $this->maximum_viewport_width || $viewport_width <= $this->maximum_viewport_width )
    222238        );
    223239    }
     
    287303                }
    288304
    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 
    294305                // The ETag of the URL Metric does not match the current ETag for the collection, so it is stale.
    295306                if ( ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) ) {
     
    330341             * Seen breadcrumbs counts.
    331342             *
    332              * @var array<int, string> $seen_breadcrumbs
     343             * @var array<int, non-empty-string> $seen_breadcrumbs
    333344             */
    334345            $seen_breadcrumbs = array();
     
    337348             * Breadcrumb counts.
    338349             *
    339              * @var array<int, int> $breadcrumb_counts
     350             * @var array<int, non-negative-int> $breadcrumb_counts
    340351             */
    341352            $breadcrumb_counts = array();
     
    490501     *             freshness_ttl: 0|positive-int,
    491502     *             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,
    494505     *             lcp_element: ?OD_Element,
    495506     *             complete: bool,
  • optimization-detective/tags/1.0.0-beta2/class-od-url-metric.php

    r3229869 r3240307  
    1717 *
    1818 * @phpstan-type ViewportRect array{
    19  *                                width: int,
    20  *                                height: int
     19 *                                width: positive-int,
     20 *                                height: positive-int
    2121 *                            }
    2222 * @phpstan-type DOMRect      array{
     
    4040 * @phpstan-type Data         array{
    4141 *                                uuid: non-empty-string,
    42  *                                etag?: non-empty-string,
     42 *                                etag: non-empty-string,
    4343 *                                url: non-empty-string,
    4444 *                                timestamp: float,
     
    6161 *
    6262 * @since 0.1.0
    63  * @access private
    6463 */
    6564class OD_URL_Metric implements JsonSerializable {
     
    6867     * Data.
    6968     *
     69     * @since 0.1.0
    7070     * @var Data
    7171     */
     
    7575     * Elements.
    7676     *
     77     * @since 0.7.0
    7778     * @var OD_Element[]
    7879     */
     
    8990    /**
    9091     * Constructor.
     92     *
     93     * @since 0.1.0
    9194     *
    9295     * @phpstan-param Data|array<string, mixed> $data Valid data or invalid data (in which case an exception is thrown).
     
    105108    /**
    106109     * Prepares data with validation and sanitization.
     110     *
     111     * @since 0.6.0
    107112     *
    108113     * @throws OD_Data_Validation_Exception When the input is invalid.
     
    172177     * @since 0.1.0
    173178     * @since 0.9.0 Added the 'etag' property to the schema.
     179     * @since 1.0.0 The 'etag' property is now required.
    174180     *
    175181     * @todo Cache the return value?
     
    231237                    'minLength'   => 32,
    232238                    'maxLength'   => 32,
    233                     'required'    => false, // To be made required in a future release.
     239                    'required'    => true,
    234240                    'readonly'    => true, // Omit from REST API.
    235241                ),
     
    249255                            'type'     => 'integer',
    250256                            'required' => true,
    251                             'minimum'  => 0,
     257                            'minimum'  => 1,
    252258                        ),
    253259                        'height' => array(
    254260                            'type'     => 'integer',
    255261                            'required' => true,
    256                             'minimum'  => 0,
     262                            'minimum'  => 1,
    257263                        ),
    258264                    ),
     
    350356     * @param array<string, mixed> $additional_properties Additional properties.
    351357     * @param string               $filter_name           Filter name used to extend.
    352      *
    353358     * @return array<string, mixed> Extended schema.
    354359     */
     
    437442     * @since 0.6.0
    438443     *
    439      * @return string UUID.
     444     * @return non-empty-string UUID.
    440445     */
    441446    public function get_uuid(): string {
     
    447452     *
    448453     * @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'];
    455460    }
    456461
     
    460465     * @since 0.1.0
    461466     *
    462      * @return string URL.
     467     * @return non-empty-string URL.
    463468     */
    464469    public function get_url(): string {
     
    482487     * @since 0.1.0
    483488     *
    484      * @return int Viewport width.
     489     * @return positive-int Viewport width.
    485490     */
    486491    public function get_viewport_width(): int {
  • optimization-detective/tags/1.0.0-beta2/class-od-visited-tag-state.php

    r3229869 r3240307  
    3131    /**
    3232     * Constructor.
     33     *
     34     * @since 1.0.0
    3335     */
    3436    public function __construct() {
  • optimization-detective/tags/1.0.0-beta2/detect.js

    r3229869 r3240307  
    9797
    9898/**
    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()`.
    100103 *
    101104 * @param {number}                 viewportWidth          - Current viewport width.
    102105 * @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 */
     108function 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 */
     131async 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 }`;
    115158}
    116159
     
    254297 * @param {boolean}                args.isDebug                    Whether to show debug messages.
    255298 * @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.
    256300 * @param {string}                 args.currentETag                Current ETag.
    257301 * @param {string}                 args.currentUrl                 Current URL.
     
    261305 * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses     URL Metric group statuses.
    262306 * @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.
    263308 * @param {string}                 args.webVitalsLibrarySrc        The URL for the web-vitals library.
    264309 * @param {CollectionDebugData}    [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode.
     
    270315    extensionModuleUrls,
    271316    restApiEndpoint,
     317    restApiNonce,
    272318    currentETag,
    273319    currentUrl,
     
    277323    urlMetricGroupStatuses,
    278324    storageLockTTL,
     325    freshnessTTL,
    279326    webVitalsLibrarySrc,
    280327    urlMetricGroupCollection,
     
    298345    }
    299346
     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
    300356    // 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 ) {
    302362        if ( isDebug ) {
    303363            log( 'No need for URL Metrics from the current viewport.' );
    304364        }
    305365        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        }
    306391    }
    307392
     
    604689    }
    605690
     691    // Finalize extensions.
    606692    if ( extensions.size > 0 ) {
    607693        /** @type {Promise[]} */
     
    656742    }
    657743
     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
    658774    // Even though the server may reject the REST API request, we still have to set the storage lock
    659775    // because we can't look at the response when sending a beacon.
    660776    setStorageLock( getCurrentTime() );
    661777
     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
    662784    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        }
    664795    }
    665796
    666797    const url = new URL( restApiEndpoint );
     798    if ( typeof restApiNonce === 'string' ) {
     799        url.searchParams.set( '_wpnonce', restApiNonce );
     800    }
    667801    url.searchParams.set( 'slug', urlMetricSlug );
    668802    url.searchParams.set( 'current_etag', currentETag );
     
    676810    navigator.sendBeacon(
    677811        url,
    678         new Blob( [ JSON.stringify( urlMetric ) ], {
     812        new Blob( [ jsonBody ], {
    679813            type: 'application/json',
    680814        } )
  • 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()}
     1const 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  
    3939 * @global WP_Query $wp_query WordPress Query object.
    4040 *
    41  * @return int|null Post ID or null if none found.
     41 * @return positive-int|null Post ID or null if none found.
    4242 */
    4343function od_get_cache_purge_post_id(): ?int {
    4444    $queried_object = get_queried_object();
    45     if ( $queried_object instanceof WP_Post ) {
     45    if ( $queried_object instanceof WP_Post && $queried_object->ID > 0 ) {
    4646        return $queried_object->ID;
    4747    }
     
    5656        &&
    5757        $wp_query->posts[0] instanceof WP_Post
     58        &&
     59        $wp_query->posts[0]->ID > 0
    5860    ) {
    5961        return $wp_query->posts[0]->ID;
     
    6971 * @access private
    7072 *
    71  * @param string                         $slug             URL Metrics slug.
     73 * @param non-empty-string               $slug             URL Metrics slug.
    7274 * @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection.
    7375 */
     
    129131            static function ( OD_URL_Metric_Group $group ): array {
    130132                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.
    132135                    'complete'             => $group->is_complete(),
    133136                );
     
    136139        ),
    137140        'storageLockTTL'         => OD_Storage_Lock::get_ttl(),
     141        'freshnessTTL'           => od_get_url_metric_freshness_ttl(),
    138142        'webVitalsLibrarySrc'    => $web_vitals_lib_src,
    139143    );
     144    if ( is_user_logged_in() ) {
     145        $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' );
     146    }
    140147    if ( WP_DEBUG ) {
    141148        $detect_args['urlMetricGroupCollection'] = $group_collection;
  • optimization-detective/tags/1.0.0-beta2/docs/extensions.md

    r3229869 r3240307  
    1111**[Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) ([GitHub](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer)):**
    1212
    13 1. Add breakpoint-specific `fetchpriority=high` preload links (`LINK[rel=preload]`) for image URLs of LCP elements:
     131. Add breakpoint-specific `fetchpriority=high` preload links (both as `LINK[rel=preload]` elements and `Link` response headers) for image URLs of LCP elements:
    1414    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))
    1515    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))
     
    2323    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))
    2424    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))
     255. 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))
    26286. 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))
    2729
  • optimization-detective/tags/1.0.0-beta2/docs/hooks.md

    r3229869 r3240307  
    103103```
    104104
    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
     107Filters how long the current IP is locked from submitting another URL metric storage REST API request.
     108
     109Filtering 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:
    108110
    109111```php
     
    113115```
    114116
     117By 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
    115119During 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:
    116120
     
    121125```
    122126
    123 ### Filter: `od_url_metric_freshness_ttl` (default: 1 day in 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
     129Filters 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:
    126130
    127131```php
    128132add_filter( 'od_url_metric_freshness_ttl', static function (): int {
    129     return WEEK_IN_SECONDS;
    130 } );
    131 ```
     133    return MONTH_IN_SECONDS;
     134} );
     135```
     136
     137Note 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.
    132138
    133139During 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:
     
    230236See 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).
    231237
    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>`)
    233239
    234240Filters the data that goes into computing the current ETag for URL Metrics.
    235241
    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.
     242The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes:
     243
     2441. The active theme and current version (for both parent and child themes).
     2452. The queried object ID, post type, and modified date.
     2463. The list of registered tag visitors.
     2474. The IDs and modified times of posts in The Loop.
     2485. The current theme template used to render the page.
     2496. The list of active plugins.
     250
     251A 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  
    2323     * Fires when extensions to Optimization Detective can be loaded and initialized.
    2424     *
     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     *
    2541     * @since 0.7.0
    2642     *
     
    3854 * @since 0.7.0
    3955 *
    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).
    4258 * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid.
    4359 */
    4460function 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' );
    4763        return null;
    4864    }
    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 {
    5774        return null;
    5875    }
    59     return join( ' and ', $media_attributes );
    6076}
    6177
  • optimization-detective/tags/1.0.0-beta2/hooks.php

    r3229869 r3240307  
    1919add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX );
    2020OD_URL_Metrics_Post_Type::add_hooks();
     21OD_Storage_Lock::add_hooks();
    2122add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' );
    2223add_action( 'wp_head', 'od_render_generator_meta_tag' );
  • optimization-detective/tags/1.0.0-beta2/load.php

    r3229869 r3240307  
    33 * Plugin Name: Optimization Detective
    44 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective
    5  * Description: Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance.
     5 * Description: Provides a framework for leveraging real user metrics to detect optimizations for improving page performance.
    66 * Requires at least: 6.6
    77 * Requires PHP: 7.2
    8  * Version: 1.0.0-beta1
     8 * Version: 1.0.0-beta2
    99 * Author: WordPress Performance Team
    1010 * Author URI: https://make.wordpress.org/performance/
     
    7272)(
    7373    'optimization_detective_pending_plugin',
    74     '1.0.0-beta1',
     74    '1.0.0-beta2',
    7575    static function ( string $version ): void {
    7676        if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
  • optimization-detective/tags/1.0.0-beta2/optimization.php

    r3229869 r3240307  
    168168        // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway).
    169169        is_embed() ||
     170        // Skip posts that aren't published yet.
     171        is_preview() ||
    170172        // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context.
    171173        is_customize_preview() ||
     
    252254     * Fires to register tag visitors before walking over the document to perform optimizations.
    253255     *
     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     *
    254327     * @since 0.3.0
    255328     *
     
    268341    $link_collection      = new OD_Link_Collection();
    269342    $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    );
    271350    $current_tag_bookmark = 'optimization_detective_current_tag';
    272351    $visitors             = iterator_to_array( $tag_visitor_registry );
  • optimization-detective/tags/1.0.0-beta2/readme.txt

    r3229869 r3240307  
    33Contributors: wordpressdotorg
    44Tested up to: 6.7
    5 Stable tag:   1.0.0-beta1
     5Stable tag:   1.0.0-beta2
    66License:      GPLv2 or later
    77License URI:  https://www.gnu.org/licenses/gpl-2.0.html
    88Tags:         performance, optimization, rum
    99
    10 Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance.
     10Provides a framework for leveraging real user metrics to detect optimizations for improving page performance.
    1111
    1212== Description ==
     
    5555
    5656== 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))
    5779
    5880= 1.0.0-beta1 =
  • optimization-detective/tags/1.0.0-beta2/site-health.php

    r3229869 r3240307  
    128128        $body    = wp_remote_retrieve_body( $response );
    129129        $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        }
    130134
    131135        $is_expected = (
     
    157161            }
    158162
    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            }
    160175        }
    161176    }
     
    239254    }
    240255
    241     wp_admin_notice(
     256    $notice = wp_get_admin_notice(
    242257        $message,
    243258        array(
     
    247262        )
    248263    );
     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    );
    249274}
    250275
     
    255280 * @access private
    256281 *
    257  * @param string $plugin_file Plugin file.
     282 * @param non-empty-string $plugin_file Plugin file.
    258283 */
    259284function 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  
    2222
    2323    /**
     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    /**
    2459     * Gets the TTL (in seconds) for the URL Metric storage lock.
    2560     *
    2661     * @since 0.1.0
    27      * @access private
    2862     *
    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.
    3064     */
    3165    public static function get_ttl(): int {
     66        $ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS;
    3267
    3368        /**
    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.
    3570         *
    36          * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable
     71         * Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable
    3772         * locking when a user is logged-in with code like the following:
    3873         *
     
    4176         *     } );
    4277         *
     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         *
    4382         * @since 0.1.0
     83         * @since 1.0.0 This now defaults to zero (0) for authorized users.
    4484         *
    45          * @param int $ttl TTL.
     85         * @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users.
    4686         */
    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 );
    4888        return max( 0, $ttl );
    4989    }
     
    5292     * Gets transient key for locking URL Metric storage (for the current IP).
    5393     *
    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.
    5697     */
    5798    public static function get_transient_key(): string {
     
    67108     *
    68109     * @since 0.1.0
    69      * @access private
    70110     */
    71111    public static function set_lock(): void {
     
    83123     *
    84124     * @since 0.1.0
    85      * @access private
    86125     *
    87126     * @return bool Whether locked.
  • optimization-detective/tags/1.0.0-beta2/storage/class-od-url-metric-store-request-context.php

    r3229869 r3240307  
    1717 *
    1818 * @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.
    2026 */
    2127final class OD_URL_Metric_Store_Request_Context {
     
    2430     * Request.
    2531     *
     32     * @since 0.7.0
    2633     * @var WP_REST_Request<array<string, mixed>>
    27      * @readonly
    2834     */
    29     public $request;
     35    private $request;
    3036
    3137    /**
    32      * ID for the URL Metric post.
     38     * ID for the od_url_metrics post.
    3339     *
    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
    3644     */
    37     public $post_id;
     45    private $url_metrics_id;
    3846
    3947    /**
    4048     * URL Metric group collection.
    4149     *
     50     * @since 0.7.0
    4251     * @var OD_URL_Metric_Group_Collection
    43      * @readonly
    4452     */
    45     public $url_metric_group_collection;
     53    private $url_metric_group_collection;
    4654
    4755    /**
    4856     * URL Metric group.
    4957     *
     58     * @since 0.7.0
    5059     * @var OD_URL_Metric_Group
    51      * @readonly
    5260     */
    53     public $url_metric_group;
     61    private $url_metric_group;
    5462
    5563    /**
    5664     * URL Metric.
    5765     *
     66     * @since 0.7.0
    5867     * @var OD_URL_Metric
    59      * @readonly
    6068     */
    61     public $url_metric;
     69    private $url_metric;
    6270
    6371    /**
    6472     * Constructor.
    6573     *
     74     * @since 0.7.0
     75     *
    6676     * @phpstan-param WP_REST_Request<array<string, mixed>> $request
    6777     *
    6878     * @param WP_REST_Request                $request                     REST API request.
    69      * @param int                            $post_id                     ID for the URL Metric post.
     79     * @param positive-int                   $url_metrics_id              ID for the URL Metric post.
    7080     * @param OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection.
    7181     * @param OD_URL_Metric_Group            $url_metric_group            URL Metric group.
    7282     * @param OD_URL_Metric                  $url_metric                  URL Metric.
    7383     */
    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 ) {
    7585        $this->request                     = $request;
    76         $this->post_id                     = $post_id;
     86        $this->url_metrics_id              = $url_metrics_id;
    7787        $this->url_metric_group_collection = $url_metric_group_collection;
    7888        $this->url_metric_group            = $url_metric_group;
    7989        $this->url_metric                  = $url_metric;
    8090    }
     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    }
    81139}
  • optimization-detective/tags/1.0.0-beta2/storage/class-od-url-metrics-post-type.php

    r3229869 r3240307  
    2424     * Post type slug.
    2525     *
     26     * @since 0.1.0
    2627     * @var string
    2728     */
     
    3132     * Event name (hook) for garbage collection of stale URL Metrics posts.
    3233     *
     34     * @since 0.1.0
    3335     * @var string
    3436     */
     
    3840     * Recurrence for garbage collection of stale URL Metrics posts.
    3941     *
     42     * @since 0.1.0
    4043     * @var string
    4144     */
     
    8588     * @since 0.1.0
    8689     *
    87      * @param string $slug URL Metrics slug.
     90     * @param non-empty-string $slug URL Metrics slug.
    8891     * @return WP_Post|null Post object if exists.
    8992     */
     
    203206     * @todo There is duplicate logic here with od_handle_rest_request().
    204207     *
    205      * @param string        $slug          Slug (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.
    208211     */
    209212    public static function store_url_metric( string $slug, OD_URL_Metric $new_url_metric ) {
     
    227230
    228231        $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 called
    231             // in the REST API endpoint where the ETag parameter is required. It is here exclusively for the sake of
    232             // 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         }
    236232
    237233        $group_collection = new OD_URL_Metric_Group_Collection(
     
    251247
    252248        $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.
    260251        );
    261252        if ( ! is_string( $post_data['post_content'] ) ) {
  • optimization-detective/tags/1.0.0-beta2/storage/data.php

    r3229869 r3240307  
    2121 * @access private
    2222 *
    23  * @return int Expiration TTL in seconds.
     23 * @return int<0, max> Expiration TTL in seconds.
    2424 */
    2525function od_get_url_metric_freshness_ttl(): int {
     
    3232     * @since 0.1.0
    3333     *
    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 );
    3737
    3838    if ( $freshness_ttl < 0 ) {
     
    5959 * This is used as a cache key for stored URL Metrics.
    6060 *
    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  *
    6361 * @since 0.1.0
    6462 * @access private
     
    118116        $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' );
    119117    }
     118
     119    // TODO: We should be able to assert that this returns an non-empty-string.
    120120    return esc_url_raw( $current_url );
    121121}
     
    132132 *
    133133 * @param array<string, mixed> $query_vars Normalized query vars.
    134  * @return string Slug.
     134 * @return non-empty-string Slug.
    135135 */
    136136function od_get_url_metrics_slug( array $query_vars ): string {
     
    199199        $queried_object_data['type'] = $queried_object->name;
    200200    }
     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 );
    201212
    202213    $data = array(
     
    231242            ),
    232243        ),
     244        'active_plugins'   => $active_plugins,
    233245        'current_template' => $current_template instanceof WP_Block_Template ? get_object_vars( $current_template ) : $current_template,
    234246    );
     
    258270 * @see od_get_url_metrics_slug()
    259271 *
    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.
    265277 */
    266278function od_get_url_metrics_storage_hmac( string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): string {
    267279    $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;
    269288}
    270289
     
    279298 * @see od_get_url_metrics_slug()
    280299 *
    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.
    286305 * @return bool Whether the HMAC is valid.
    287306 */
     
    361380 * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13
    362381 *
    363  * @return int[] Breakpoint max widths, sorted in ascending order.
     382 * @return positive-int[] Breakpoint max widths, sorted in ascending order.
    364383 */
    365384function od_get_breakpoint_max_widths(): array {
     
    369388        static function ( $original_breakpoint ) use ( $function_name ): int {
    370389            $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 ) {
    385391                $breakpoint = 1;
    386392                _doing_it_wrong(
     
    401407         * Filters the breakpoint max widths to group URL Metrics for various viewports.
    402408         *
    403          * A breakpoint must be greater than zero and less than PHP_INT_MAX. This array may be empty in which case there
     409         * A breakpoint must be greater than zero. This array may be empty in which case there
    404410         * are no responsive breakpoints and all URL Metrics are collected in a single group.
    405411         *
    406412         * @since 0.1.0
    407413         *
    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].
    409415         */
    410416        array_map( 'intval', (array) apply_filters( 'od_breakpoint_max_widths', array( 480, 600, 782 ) ) )
     
    426432 * @access private
    427433 *
    428  * @return int Sample size.
     434 * @return int<1, max> Sample size.
    429435 */
    430436function od_get_url_metrics_breakpoint_sample_size(): int {
  • optimization-detective/tags/1.0.0-beta2/storage/rest-api.php

    r3229869 r3240307  
    1616 * Namespace for optimization-detective.
    1717 *
     18 * @since 0.1.0
     19 * @access private
    1820 * @var string
    1921 */
     
    2729 * create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug.
    2830 *
     31 * @since 0.1.0
     32 * @access private
    2933 * @link https://google.aip.dev/136
    3034 * @var string
     
    7377            'pattern'           => '^[0-9a-f]+\z',
    7478            '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 ) ) {
    7680                    return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) );
    7781                }
     
    215219    }
    216220
     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
    217242    // 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.
    218243    $result = OD_URL_Metrics_Post_Type::store_url_metric(
  • optimization-detective/tags/1.0.0-beta2/types.ts

    r3229869 r3240307  
    3535export interface URLMetricGroupStatus {
    3636    minimumViewportWidth: number;
     37    maximumViewportWidth: number | null;
    3738    complete: boolean;
    3839}
  • optimization-detective/trunk/class-od-data-validation-exception.php

    r3229869 r3240307  
    1717 *
    1818 * @since 0.1.0
    19  * @access private
    2019 */
    2120final class OD_Data_Validation_Exception extends Exception {}
  • optimization-detective/trunk/class-od-element.php

    r3229869 r3240307  
    2222 *
    2323 * @since 0.7.0
    24  * @access private
    2524 */
    2625class OD_Element implements ArrayAccess, JsonSerializable {
  • optimization-detective/trunk/class-od-html-tag-processor.php

    r3229869 r3240307  
    1717 *
    1818 * @since 0.1.1
    19  * @access private
    2019 */
    2120final class OD_HTML_Tag_Processor extends WP_HTML_Tag_Processor {
     
    150149     *
    151150     * @since 0.4.0
    152      * @var string[]
     151     * @var non-empty-string[]
    153152     */
    154153    private $open_stack_tags = array();
     
    161160     *
    162161     * @since 1.0.0
    163      * @var array<array<string, string>>
     162     * @var array<array<non-empty-string, string>>
    164163     */
    165164    private $open_stack_attributes = array();
     
    169168     *
    170169     * @since 0.4.0
    171      * @var int[]
     170     * @var non-negative-int[]
    172171     */
    173172    private $open_stack_indices = array();
     
    182181     *
    183182     * @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[]}>
    185184     */
    186185    private $bookmarked_open_stacks = array();
     
    222221     *
    223222     * @since 0.4.0
    224      * @var array<string, string[]>
     223     * @var array<non-empty-string, string[]>
    225224     */
    226225    private $buffered_text_replacements = array();
     
    239238     *
    240239     * @since 0.6.0
    241      * @var int
     240     * @var non-negative-int
    242241     * @see self::next_token()
    243242     * @see self::seek()
     
    293292     * @since 0.4.0
    294293     *
    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.
    296295     * @return bool Whether to expect a closer for the tag.
    297296     */
     
    337336            return true;
    338337        }
     338        /**
     339         * Tag name.
     340         *
     341         * @var non-empty-string $tag_name
     342         */
    339343
    340344        if ( $this->previous_tag_without_closer ) {
     
    423427     * @see self::seek()
    424428     *
    425      * @return int Count of times the cursor has moved.
     429     * @return non-negative-int Count of times the cursor has moved.
    426430     */
    427431    public function get_cursor_move_count(): int {
     
    459463     * @since 0.4.0
    460464     *
    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.
    463467     * @return bool Whether an attribute was set.
    464468     */
     
    490494     * @see WP_HTML_Processor::get_current_depth()
    491495     *
    492      * @return int Nesting-depth of current location in the document.
     496     * @return non-negative-int Nesting-depth of current location in the document.
    493497     */
    494498    public function get_current_depth(): int {
     
    566570     * @since 0.9.0 Renamed from get_breadcrumbs() to get_indexed_breadcrumbs().
    567571     *
    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.
    569573     */
    570574    private function get_indexed_breadcrumbs(): Generator {
     
    585589     * @since 1.0.0
    586590     *
    587      * @return array<string, string> Disambiguating attributes.
     591     * @return array<non-empty-string, string> Disambiguating attributes.
    588592     */
    589593    private function get_disambiguating_attributes(): array {
     
    618622     * @see WP_HTML_Processor::get_breadcrumbs()
    619623     *
    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.
    621625     */
    622626    public function get_breadcrumbs(): array {
  • optimization-detective/trunk/class-od-link-collection.php

    r3229869 r3240307  
    1919 *                   attributes: LinkAttributes,
    2020 *                   minimum_viewport_width: int<0, max>|null,
    21  *                   maximum_viewport_width: positive-int|null
     21 *                   maximum_viewport_width: int<1, max>|null
    2222 *               }
    2323 *
     
    3838 * @since 0.3.0
    3939 * @since 0.4.0 Renamed from OD_Preload_Link_Collection.
    40  * @access private
    4140 */
    4241final class OD_Link_Collection implements Countable {
     
    4544     * Links grouped by rel type.
    4645     *
     46     * @since 0.4.0
     47     *
    4748     * @var array<string, Link[]>
    4849     */
     
    5253     * Adds link.
    5354     *
     55     * @since 0.3.0
     56     *
    5457     * @phpstan-param LinkAttributes $attributes
    5558     *
    56      * @param array             $attributes             Attributes.
    57      * @param int<0, max>|null  $minimum_viewport_width Minimum width or null if not bounded or relevant.
    58      * @param positive-int|null $maximum_viewport_width Maximum width or 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.
    5962     *
    6063     * @throws InvalidArgumentException When invalid arguments are provided.
     
    112115     * together. Also, add media attributes to the links.
    113116     *
     117     * @since 0.4.0
     118     *
    114119     * @return LinkAttributes[] Prepared links with adjacent-duplicates merged together and media attributes added.
    115120     */
     
    133138    /**
    134139     * Merges consecutive links.
     140     *
     141     * @since 0.4.0
    135142     *
    136143     * @param Link[] $links Links.
     
    195202                    is_int( $last_link['maximum_viewport_width'] )
    196203                    &&
    197                     $last_link['maximum_viewport_width'] + 1 === $link['minimum_viewport_width']
     204                    $last_link['maximum_viewport_width'] === $link['minimum_viewport_width']
    198205                ) {
    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'] );
    200207
    201208                    // Update the last link with the new maximum viewport width.
     
    229236     * Gets the HTML for the link tags.
    230237     *
     238     * @since 0.3.0
     239     *
    231240     * @return string Link tags HTML.
    232241     */
     
    250259     * Constructs the Link HTTP response header.
    251260     *
    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.
    253264     */
    254265    public function get_response_header(): ?string {
     
    256267
    257268        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'] . '>';
    261297            unset( $link['href'] );
    262298            foreach ( $link as $name => $value ) {
     
    286322
    287323    /**
     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    /**
    288346     * Counts the links.
     347     *
     348     * @since 0.3.0
    289349     *
    290350     * @return non-negative-int Link count.
  • optimization-detective/trunk/class-od-tag-visitor-context.php

    r3229869 r3240307  
    1818 * @since 0.4.0
    1919 *
    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.
    2125 */
    2226final class OD_Tag_Visitor_Context {
     
    2731     * @since 0.4.0
    2832     * @var OD_HTML_Tag_Processor
    29      * @readonly
    3033     */
    31     public $processor;
     34    private $processor;
    3235
    3336    /**
     
    3639     * @since 0.4.0
    3740     * @var OD_URL_Metric_Group_Collection
    38      * @readonly
    3941     */
    40     public $url_metric_group_collection;
     42    private $url_metric_group_collection;
    4143
    4244    /**
     
    4547     * @since 0.4.0
    4648     * @var OD_Link_Collection
    47      * @readonly
    4849     */
    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;
    5061
    5162    /**
    5263     * Visited tag state.
     64     *
     65     * Important: This object is not exposed directly by the getter. It is only exposed via {@see self::track_tag()}.
    5366     *
    5467     * @since 1.0.0
     
    6679     * @param OD_Link_Collection             $link_collection             Link collection.
    6780     * @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.
    6882     */
    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 ) {
    7084        $this->processor                   = $processor;
    7185        $this->url_metric_group_collection = $url_metric_group_collection;
    7286        $this->link_collection             = $link_collection;
    7387        $this->visited_tag_state           = $visited_tag_state;
     88        $this->url_metrics_id              = $url_metrics_id;
    7489    }
    7590
     
    86101
    87102    /**
    88      * Gets deprecated property.
     103     * Gets a property.
    89104     *
    90105     * @since 0.7.0
    91      * @todo Remove this when no plugins are possibly referring to the url_metrics_group_collection property anymore.
    92106     *
    93107     * @param string $name Property name.
    94      * @return OD_URL_Metric_Group_Collection URL Metric group collection.
     108     * @return mixed Property value.
    95109     *
    96110     * @throws Error When property is unknown.
    97111     */
    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                        )
    107145                    )
    108                 ),
    109                 'optimization-detective 0.7.0'
    110             );
    111             return $this->url_metric_group_collection;
     146                );
    112147        }
    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__ . '::$' . $name
    119                 )
    120             )
    121         );
    122148    }
    123149}
  • optimization-detective/trunk/class-od-tag-visitor-registry.php

    r3229869 r3240307  
    2121 *
    2222 * @since 0.3.0
    23  * @access private
    2423 */
    2524final class OD_Tag_Visitor_Registry implements Countable, IteratorAggregate {
     
    2827     * Visitors.
    2928     *
    30      * @var array<string, TagVisitorCallback>
     29     * @since 0.3.0
     30     *
     31     * @var array<non-empty-string, TagVisitorCallback>
    3132     */
    3233    private $visitors = array();
     
    3536     * Registers a tag visitor.
    3637     *
     38     * @since 0.3.0
     39     *
    3740     * @phpstan-param TagVisitorCallback $tag_visitor_callback
    3841     *
    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.
    4144     */
    4245    public function register( string $id, callable $tag_visitor_callback ): void {
     
    4750     * Determines if a visitor has been registered.
    4851     *
    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.
    5055     * @return bool Whether registered.
    5156     */
     
    5762     * Gets a registered visitor.
    5863     *
    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.
    6067     * @return TagVisitorCallback|null Whether registered.
    6168     */
     
    7077     * Unregisters a tag visitor.
    7178     *
    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.
    7382     * @return bool Whether a tag visitor was unregistered.
    7483     */
     
    8493     * Returns an iterator for the URL Metrics in the group.
    8594     *
     95     * @since 0.3.0
     96     *
    8697     * @return ArrayIterator<string, TagVisitorCallback> ArrayIterator for tag visitors.
    8798     */
     
    93104     * Counts the URL Metrics in the group.
    94105     *
     106     * @since 0.3.0
     107     *
    95108     * @return int<0, max> URL Metric count.
    96109     */
  • optimization-detective/trunk/class-od-url-metric-group-collection.php

    r3229869 r3240307  
    1919 *
    2020 * @since 0.1.0
    21  * @access private
    2221 */
    2322final class OD_URL_Metric_Group_Collection implements Countable, IteratorAggregate, JsonSerializable {
     
    3231     * in this case, in which every single URL Metric is added.
    3332     *
     33     * @since 0.1.0
    3434     * @var OD_URL_Metric_Group[]
    3535     * @phpstan-var non-empty-array<OD_URL_Metric_Group>
     
    4848     * Breakpoints in max widths.
    4949     *
    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.
    5653     *
    5754     * This array may be empty in which case there are no responsive breakpoints and all URL Metrics are collected in a
    5855     * single group.
    5956     *
    60      * @var int[]
    61      * @phpstan-var positive-int[]
     57     * @since 0.1.0
     58     * @var positive-int[]
    6259     */
    6360    private $breakpoints;
     
    6663     * Sample size for URL Metrics for a given breakpoint.
    6764     *
    68      * @var int
    69      * @phpstan-var positive-int
     65     * @since 0.1.0
     66     * @var int<1, max>
    7067     */
    7168    private $sample_size;
     
    7673     * A freshness age of zero means a URL Metric will always be considered stale.
    7774     *
    78      * @var int
    79      * @phpstan-var 0|positive-int
     75     * @since 0.1.0
     76     * @var int<0, max>
    8077     */
    8178    private $freshness_ttl;
     
    8481     * Result cache.
    8582     *
     83     * @since 0.3.0
    8684     * @var array{
    8785     *          get_group_for_viewport_width?: array<int, OD_URL_Metric_Group>,
     
    9290     *          get_common_lcp_element?: OD_Element|null,
    9391     *          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>>,
    9593     *          get_all_elements_positioned_in_any_initial_viewport?: array<string, bool>,
    9694     *      }
     
    10199     * Constructor.
    102100     *
     101     * @since 0.1.0
     102     *
    103103     * @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
    104108     *
    105109     * @param OD_URL_Metric[]  $url_metrics   URL Metrics.
     
    128132        $breakpoints = array_values( array_unique( $breakpoints, SORT_NUMERIC ) );
    129133        foreach ( $breakpoints as $breakpoint ) {
    130             if ( ! is_int( $breakpoint ) || $breakpoint < 1 || PHP_INT_MAX === $breakpoint ) {
     134            if ( ! is_int( $breakpoint ) || $breakpoint < 1 ) {
    131135                throw new InvalidArgumentException(
    132136                    esc_html(
     
    134138                            /* translators: %d is the invalid breakpoint */
    135139                            __(
    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',
    137141                                'optimization-detective'
    138142                            ),
     
    197201
    198202    /**
     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    /**
    199236     * Gets the first URL Metric group.
    200237     *
     
    216253     * This group normally represents viewports for desktop devices.  This group always has a minimum viewport width
    217254     * 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.
    219256     *
    220257     * @since 0.7.0
     
    245282     */
    246283    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 );
    254291        return $groups;
    255292    }
     
    273310        }
    274311        // @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.
    276313        throw new InvalidArgumentException(
    277314            esc_html__( 'No group available to add URL Metric to.', 'optimization-detective' )
     
    286323     * @throws InvalidArgumentException When there is no group for the provided viewport width. This would only happen if a negative width is provided.
    287324     *
    288      * @param int $viewport_width Viewport width.
     325     * @param positive-int $viewport_width Viewport width.
    289326     * @return OD_URL_Metric_Group URL Metric group for the viewport width.
    290327     */
     
    301338            }
    302339            // @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.
    304341            throw new InvalidArgumentException(
    305342                esc_html(
     
    496533     * @since 0.7.0
    497534     *
    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.
    499536     */
    500537    public function get_xpath_elements_map(): array {
     
    668705     *             groups: array<int, array{
    669706     *                 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,
    672709     *                 complete: bool,
    673710     *                 url_metrics: OD_URL_Metric[]
  • optimization-detective/trunk/class-od-url-metric-group.php

    r3229869 r3240307  
    1919 *
    2020 * @since 0.1.0
    21  * @access private
    2221 */
    2322final class OD_URL_Metric_Group implements IteratorAggregate, Countable, JsonSerializable {
     
    3332
    3433    /**
    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>
    4139     */
    4240    private $minimum_viewport_width;
    4341
    4442    /**
    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
    5148     */
    5249    private $maximum_viewport_width;
     
    5754     * @since 0.1.0
    5855     *
    59      * @var int
    60      * @phpstan-var positive-int
     56     * @var int<1, max>
    6157     */
    6258    private $sample_size;
     
    6763     * @since 0.1.0
    6864     *
    69      * @var int
    70      * @phpstan-var 0|positive-int
     65     * @var int<0, max>
    7166     */
    7267    private $freshness_ttl;
     
    10095     * This class should never be directly constructed. It should only be constructed by the {@see OD_URL_Metric_Group_Collection::create_groups()}.
    10196     *
     97     * @since 0.1.0
     98     *
    10299     * @access private
    103100     * @throws InvalidArgumentException If arguments are invalid.
    104101     *
     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     *
    105107     * @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.
    108110     * @param int                            $sample_size            Sample size for the maximum number of viewports in a group between breakpoints.
    109111     * @param int                            $freshness_ttl          Freshness age (TTL) for a given URL Metric.
    110112     * @param OD_URL_Metric_Group_Collection $collection             Collection that this instance belongs to.
    111113     */
    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 ) {
    113115        if ( $minimum_viewport_width < 0 ) {
    114116            throw new InvalidArgumentException(
     
    116118            );
    117119        }
    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            }
    127131        }
    128132        $this->minimum_viewport_width = $minimum_viewport_width;
     
    159163
    160164    /**
    161      * Gets the minimum possible viewport width (inclusive).
     165     * Gets the minimum possible viewport width (exclusive).
    162166     *
    163167     * @since 0.1.0
    164168     *
    165169     * @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).
    167171     */
    168172    public function get_minimum_viewport_width(): int {
     
    176180     *
    177181     * @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 {
    181185        return $this->maximum_viewport_width;
    182186    }
     
    188192     *
    189193     * @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.
    192195     */
    193196    public function get_sample_size(): int {
     
    201204     *
    202205     * @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.
    205207     */
    206208    public function get_freshness_ttl(): int {
     
    209211
    210212    /**
    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
    214230     *
    215231     * @param int $viewport_width Viewport width.
     
    218234    public function is_viewport_width_in_range( int $viewport_width ): bool {
    219235        return (
    220             $viewport_width >= $this->minimum_viewport_width &&
    221             $viewport_width <= $this->maximum_viewport_width
     236            $viewport_width > $this->minimum_viewport_width &&
     237            ( null === $this->maximum_viewport_width || $viewport_width <= $this->maximum_viewport_width )
    222238        );
    223239    }
     
    287303                }
    288304
    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 
    294305                // The ETag of the URL Metric does not match the current ETag for the collection, so it is stale.
    295306                if ( ! hash_equals( $url_metric->get_etag(), $this->collection->get_current_etag() ) ) {
     
    330341             * Seen breadcrumbs counts.
    331342             *
    332              * @var array<int, string> $seen_breadcrumbs
     343             * @var array<int, non-empty-string> $seen_breadcrumbs
    333344             */
    334345            $seen_breadcrumbs = array();
     
    337348             * Breadcrumb counts.
    338349             *
    339              * @var array<int, int> $breadcrumb_counts
     350             * @var array<int, non-negative-int> $breadcrumb_counts
    340351             */
    341352            $breadcrumb_counts = array();
     
    490501     *             freshness_ttl: 0|positive-int,
    491502     *             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,
    494505     *             lcp_element: ?OD_Element,
    495506     *             complete: bool,
  • optimization-detective/trunk/class-od-url-metric.php

    r3229869 r3240307  
    1717 *
    1818 * @phpstan-type ViewportRect array{
    19  *                                width: int,
    20  *                                height: int
     19 *                                width: positive-int,
     20 *                                height: positive-int
    2121 *                            }
    2222 * @phpstan-type DOMRect      array{
     
    4040 * @phpstan-type Data         array{
    4141 *                                uuid: non-empty-string,
    42  *                                etag?: non-empty-string,
     42 *                                etag: non-empty-string,
    4343 *                                url: non-empty-string,
    4444 *                                timestamp: float,
     
    6161 *
    6262 * @since 0.1.0
    63  * @access private
    6463 */
    6564class OD_URL_Metric implements JsonSerializable {
     
    6867     * Data.
    6968     *
     69     * @since 0.1.0
    7070     * @var Data
    7171     */
     
    7575     * Elements.
    7676     *
     77     * @since 0.7.0
    7778     * @var OD_Element[]
    7879     */
     
    8990    /**
    9091     * Constructor.
     92     *
     93     * @since 0.1.0
    9194     *
    9295     * @phpstan-param Data|array<string, mixed> $data Valid data or invalid data (in which case an exception is thrown).
     
    105108    /**
    106109     * Prepares data with validation and sanitization.
     110     *
     111     * @since 0.6.0
    107112     *
    108113     * @throws OD_Data_Validation_Exception When the input is invalid.
     
    172177     * @since 0.1.0
    173178     * @since 0.9.0 Added the 'etag' property to the schema.
     179     * @since 1.0.0 The 'etag' property is now required.
    174180     *
    175181     * @todo Cache the return value?
     
    231237                    'minLength'   => 32,
    232238                    'maxLength'   => 32,
    233                     'required'    => false, // To be made required in a future release.
     239                    'required'    => true,
    234240                    'readonly'    => true, // Omit from REST API.
    235241                ),
     
    249255                            'type'     => 'integer',
    250256                            'required' => true,
    251                             'minimum'  => 0,
     257                            'minimum'  => 1,
    252258                        ),
    253259                        'height' => array(
    254260                            'type'     => 'integer',
    255261                            'required' => true,
    256                             'minimum'  => 0,
     262                            'minimum'  => 1,
    257263                        ),
    258264                    ),
     
    350356     * @param array<string, mixed> $additional_properties Additional properties.
    351357     * @param string               $filter_name           Filter name used to extend.
    352      *
    353358     * @return array<string, mixed> Extended schema.
    354359     */
     
    437442     * @since 0.6.0
    438443     *
    439      * @return string UUID.
     444     * @return non-empty-string UUID.
    440445     */
    441446    public function get_uuid(): string {
     
    447452     *
    448453     * @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'];
    455460    }
    456461
     
    460465     * @since 0.1.0
    461466     *
    462      * @return string URL.
     467     * @return non-empty-string URL.
    463468     */
    464469    public function get_url(): string {
     
    482487     * @since 0.1.0
    483488     *
    484      * @return int Viewport width.
     489     * @return positive-int Viewport width.
    485490     */
    486491    public function get_viewport_width(): int {
  • optimization-detective/trunk/class-od-visited-tag-state.php

    r3229869 r3240307  
    3131    /**
    3232     * Constructor.
     33     *
     34     * @since 1.0.0
    3335     */
    3436    public function __construct() {
  • optimization-detective/trunk/detect.js

    r3229869 r3240307  
    9797
    9898/**
    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()`.
    100103 *
    101104 * @param {number}                 viewportWidth          - Current viewport width.
    102105 * @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 */
     108function 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 */
     131async 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 }`;
    115158}
    116159
     
    254297 * @param {boolean}                args.isDebug                    Whether to show debug messages.
    255298 * @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.
    256300 * @param {string}                 args.currentETag                Current ETag.
    257301 * @param {string}                 args.currentUrl                 Current URL.
     
    261305 * @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses     URL Metric group statuses.
    262306 * @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.
    263308 * @param {string}                 args.webVitalsLibrarySrc        The URL for the web-vitals library.
    264309 * @param {CollectionDebugData}    [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode.
     
    270315    extensionModuleUrls,
    271316    restApiEndpoint,
     317    restApiNonce,
    272318    currentETag,
    273319    currentUrl,
     
    277323    urlMetricGroupStatuses,
    278324    storageLockTTL,
     325    freshnessTTL,
    279326    webVitalsLibrarySrc,
    280327    urlMetricGroupCollection,
     
    298345    }
    299346
     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
    300356    // 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 ) {
    302362        if ( isDebug ) {
    303363            log( 'No need for URL Metrics from the current viewport.' );
    304364        }
    305365        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        }
    306391    }
    307392
     
    604689    }
    605690
     691    // Finalize extensions.
    606692    if ( extensions.size > 0 ) {
    607693        /** @type {Promise[]} */
     
    656742    }
    657743
     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
    658774    // Even though the server may reject the REST API request, we still have to set the storage lock
    659775    // because we can't look at the response when sending a beacon.
    660776    setStorageLock( getCurrentTime() );
    661777
     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
    662784    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        }
    664795    }
    665796
    666797    const url = new URL( restApiEndpoint );
     798    if ( typeof restApiNonce === 'string' ) {
     799        url.searchParams.set( '_wpnonce', restApiNonce );
     800    }
    667801    url.searchParams.set( 'slug', urlMetricSlug );
    668802    url.searchParams.set( 'current_etag', currentETag );
     
    676810    navigator.sendBeacon(
    677811        url,
    678         new Blob( [ JSON.stringify( urlMetric ) ], {
     812        new Blob( [ jsonBody ], {
    679813            type: 'application/json',
    680814        } )
  • 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()}
     1const 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  
    3939 * @global WP_Query $wp_query WordPress Query object.
    4040 *
    41  * @return int|null Post ID or null if none found.
     41 * @return positive-int|null Post ID or null if none found.
    4242 */
    4343function od_get_cache_purge_post_id(): ?int {
    4444    $queried_object = get_queried_object();
    45     if ( $queried_object instanceof WP_Post ) {
     45    if ( $queried_object instanceof WP_Post && $queried_object->ID > 0 ) {
    4646        return $queried_object->ID;
    4747    }
     
    5656        &&
    5757        $wp_query->posts[0] instanceof WP_Post
     58        &&
     59        $wp_query->posts[0]->ID > 0
    5860    ) {
    5961        return $wp_query->posts[0]->ID;
     
    6971 * @access private
    7072 *
    71  * @param string                         $slug             URL Metrics slug.
     73 * @param non-empty-string               $slug             URL Metrics slug.
    7274 * @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection.
    7375 */
     
    129131            static function ( OD_URL_Metric_Group $group ): array {
    130132                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.
    132135                    'complete'             => $group->is_complete(),
    133136                );
     
    136139        ),
    137140        'storageLockTTL'         => OD_Storage_Lock::get_ttl(),
     141        'freshnessTTL'           => od_get_url_metric_freshness_ttl(),
    138142        'webVitalsLibrarySrc'    => $web_vitals_lib_src,
    139143    );
     144    if ( is_user_logged_in() ) {
     145        $detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' );
     146    }
    140147    if ( WP_DEBUG ) {
    141148        $detect_args['urlMetricGroupCollection'] = $group_collection;
  • optimization-detective/trunk/docs/extensions.md

    r3229869 r3240307  
    1111**[Image Prioritizer](https://wordpress.org/plugins/image-prioritizer/) ([GitHub](https://github.com/WordPress/performance/tree/trunk/plugins/image-prioritizer)):**
    1212
    13 1. Add breakpoint-specific `fetchpriority=high` preload links (`LINK[rel=preload]`) for image URLs of LCP elements:
     131. Add breakpoint-specific `fetchpriority=high` preload links (both as `LINK[rel=preload]` elements and `Link` response headers) for image URLs of LCP elements:
    1414    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))
    1515    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))
     
    2323    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))
    2424    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))
     255. 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))
    26286. 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))
    2729
  • optimization-detective/trunk/docs/hooks.md

    r3229869 r3240307  
    103103```
    104104
    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
     107Filters how long the current IP is locked from submitting another URL metric storage REST API request.
     108
     109Filtering 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:
    108110
    109111```php
     
    113115```
    114116
     117By 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
    115119During 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:
    116120
     
    121125```
    122126
    123 ### Filter: `od_url_metric_freshness_ttl` (default: 1 day in 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
     129Filters 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:
    126130
    127131```php
    128132add_filter( 'od_url_metric_freshness_ttl', static function (): int {
    129     return WEEK_IN_SECONDS;
    130 } );
    131 ```
     133    return MONTH_IN_SECONDS;
     134} );
     135```
     136
     137Note 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.
    132138
    133139During 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:
     
    230236See 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).
    231237
    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>`)
    233239
    234240Filters the data that goes into computing the current ETag for URL Metrics.
    235241
    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.
     242The ETag is a unique identifier that changes whenever the underlying data used to generate it changes. By default, the ETag calculation includes:
     243
     2441. The active theme and current version (for both parent and child themes).
     2452. The queried object ID, post type, and modified date.
     2463. The list of registered tag visitors.
     2474. The IDs and modified times of posts in The Loop.
     2485. The current theme template used to render the page.
     2496. The list of active plugins.
     250
     251A 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  
    2323     * Fires when extensions to Optimization Detective can be loaded and initialized.
    2424     *
     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     *
    2541     * @since 0.7.0
    2642     *
     
    3854 * @since 0.7.0
    3955 *
    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).
    4258 * @return non-empty-string|null Media query, or null if the min/max were both unspecified or invalid.
    4359 */
    4460function 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' );
    4763        return null;
    4864    }
    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 {
    5774        return null;
    5875    }
    59     return join( ' and ', $media_attributes );
    6076}
    6177
  • optimization-detective/trunk/hooks.php

    r3229869 r3240307  
    1919add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX );
    2020OD_URL_Metrics_Post_Type::add_hooks();
     21OD_Storage_Lock::add_hooks();
    2122add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' );
    2223add_action( 'wp_head', 'od_render_generator_meta_tag' );
  • optimization-detective/trunk/load.php

    r3229869 r3240307  
    33 * Plugin Name: Optimization Detective
    44 * Plugin URI: https://github.com/WordPress/performance/tree/trunk/plugins/optimization-detective
    5  * Description: Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance.
     5 * Description: Provides a framework for leveraging real user metrics to detect optimizations for improving page performance.
    66 * Requires at least: 6.6
    77 * Requires PHP: 7.2
    8  * Version: 1.0.0-beta1
     8 * Version: 1.0.0-beta2
    99 * Author: WordPress Performance Team
    1010 * Author URI: https://make.wordpress.org/performance/
     
    7272)(
    7373    'optimization_detective_pending_plugin',
    74     '1.0.0-beta1',
     74    '1.0.0-beta2',
    7575    static function ( string $version ): void {
    7676        if ( defined( 'OPTIMIZATION_DETECTIVE_VERSION' ) ) {
  • optimization-detective/trunk/optimization.php

    r3229869 r3240307  
    168168        // So it's better to just avoid attempting to optimize Post Embed responses (which don't need optimization anyway).
    169169        is_embed() ||
     170        // Skip posts that aren't published yet.
     171        is_preview() ||
    170172        // Since injection of inline-editing controls interfere with breadcrumbs, while also just not necessary in this context.
    171173        is_customize_preview() ||
     
    252254     * Fires to register tag visitors before walking over the document to perform optimizations.
    253255     *
     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     *
    254327     * @since 0.3.0
    255328     *
     
    268341    $link_collection      = new OD_Link_Collection();
    269342    $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    );
    271350    $current_tag_bookmark = 'optimization_detective_current_tag';
    272351    $visitors             = iterator_to_array( $tag_visitor_registry );
  • optimization-detective/trunk/readme.txt

    r3229869 r3240307  
    33Contributors: wordpressdotorg
    44Tested up to: 6.7
    5 Stable tag:   1.0.0-beta1
     5Stable tag:   1.0.0-beta2
    66License:      GPLv2 or later
    77License URI:  https://www.gnu.org/licenses/gpl-2.0.html
    88Tags:         performance, optimization, rum
    99
    10 Provides an API for leveraging real user metrics to detect optimizations to apply on the frontend to improve page performance.
     10Provides a framework for leveraging real user metrics to detect optimizations for improving page performance.
    1111
    1212== Description ==
     
    5555
    5656== 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))
    5779
    5880= 1.0.0-beta1 =
  • optimization-detective/trunk/site-health.php

    r3229869 r3240307  
    128128        $body    = wp_remote_retrieve_body( $response );
    129129        $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        }
    130134
    131135        $is_expected = (
     
    157161            }
    158162
    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            }
    160175        }
    161176    }
     
    239254    }
    240255
    241     wp_admin_notice(
     256    $notice = wp_get_admin_notice(
    242257        $message,
    243258        array(
     
    247262        )
    248263    );
     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    );
    249274}
    250275
     
    255280 * @access private
    256281 *
    257  * @param string $plugin_file Plugin file.
     282 * @param non-empty-string $plugin_file Plugin file.
    258283 */
    259284function 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  
    2222
    2323    /**
     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    /**
    2459     * Gets the TTL (in seconds) for the URL Metric storage lock.
    2560     *
    2661     * @since 0.1.0
    27      * @access private
    2862     *
    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.
    3064     */
    3165    public static function get_ttl(): int {
     66        $ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS;
    3267
    3368        /**
    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.
    3570         *
    36          * Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable
     71         * Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable
    3772         * locking when a user is logged-in with code like the following:
    3873         *
     
    4176         *     } );
    4277         *
     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         *
    4382         * @since 0.1.0
     83         * @since 1.0.0 This now defaults to zero (0) for authorized users.
    4484         *
    45          * @param int $ttl TTL.
     85         * @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users.
    4686         */
    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 );
    4888        return max( 0, $ttl );
    4989    }
     
    5292     * Gets transient key for locking URL Metric storage (for the current IP).
    5393     *
    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.
    5697     */
    5798    public static function get_transient_key(): string {
     
    67108     *
    68109     * @since 0.1.0
    69      * @access private
    70110     */
    71111    public static function set_lock(): void {
     
    83123     *
    84124     * @since 0.1.0
    85      * @access private
    86125     *
    87126     * @return bool Whether locked.
  • optimization-detective/trunk/storage/class-od-url-metric-store-request-context.php

    r3229869 r3240307  
    1717 *
    1818 * @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.
    2026 */
    2127final class OD_URL_Metric_Store_Request_Context {
     
    2430     * Request.
    2531     *
     32     * @since 0.7.0
    2633     * @var WP_REST_Request<array<string, mixed>>
    27      * @readonly
    2834     */
    29     public $request;
     35    private $request;
    3036
    3137    /**
    32      * ID for the URL Metric post.
     38     * ID for the od_url_metrics post.
    3339     *
    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
    3644     */
    37     public $post_id;
     45    private $url_metrics_id;
    3846
    3947    /**
    4048     * URL Metric group collection.
    4149     *
     50     * @since 0.7.0
    4251     * @var OD_URL_Metric_Group_Collection
    43      * @readonly
    4452     */
    45     public $url_metric_group_collection;
     53    private $url_metric_group_collection;
    4654
    4755    /**
    4856     * URL Metric group.
    4957     *
     58     * @since 0.7.0
    5059     * @var OD_URL_Metric_Group
    51      * @readonly
    5260     */
    53     public $url_metric_group;
     61    private $url_metric_group;
    5462
    5563    /**
    5664     * URL Metric.
    5765     *
     66     * @since 0.7.0
    5867     * @var OD_URL_Metric
    59      * @readonly
    6068     */
    61     public $url_metric;
     69    private $url_metric;
    6270
    6371    /**
    6472     * Constructor.
    6573     *
     74     * @since 0.7.0
     75     *
    6676     * @phpstan-param WP_REST_Request<array<string, mixed>> $request
    6777     *
    6878     * @param WP_REST_Request                $request                     REST API request.
    69      * @param int                            $post_id                     ID for the URL Metric post.
     79     * @param positive-int                   $url_metrics_id              ID for the URL Metric post.
    7080     * @param OD_URL_Metric_Group_Collection $url_metric_group_collection URL Metric group collection.
    7181     * @param OD_URL_Metric_Group            $url_metric_group            URL Metric group.
    7282     * @param OD_URL_Metric                  $url_metric                  URL Metric.
    7383     */
    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 ) {
    7585        $this->request                     = $request;
    76         $this->post_id                     = $post_id;
     86        $this->url_metrics_id              = $url_metrics_id;
    7787        $this->url_metric_group_collection = $url_metric_group_collection;
    7888        $this->url_metric_group            = $url_metric_group;
    7989        $this->url_metric                  = $url_metric;
    8090    }
     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    }
    81139}
  • optimization-detective/trunk/storage/class-od-url-metrics-post-type.php

    r3229869 r3240307  
    2424     * Post type slug.
    2525     *
     26     * @since 0.1.0
    2627     * @var string
    2728     */
     
    3132     * Event name (hook) for garbage collection of stale URL Metrics posts.
    3233     *
     34     * @since 0.1.0
    3335     * @var string
    3436     */
     
    3840     * Recurrence for garbage collection of stale URL Metrics posts.
    3941     *
     42     * @since 0.1.0
    4043     * @var string
    4144     */
     
    8588     * @since 0.1.0
    8689     *
    87      * @param string $slug URL Metrics slug.
     90     * @param non-empty-string $slug URL Metrics slug.
    8891     * @return WP_Post|null Post object if exists.
    8992     */
     
    203206     * @todo There is duplicate logic here with od_handle_rest_request().
    204207     *
    205      * @param string        $slug          Slug (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.
    208211     */
    209212    public static function store_url_metric( string $slug, OD_URL_Metric $new_url_metric ) {
     
    227230
    228231        $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 called
    231             // in the REST API endpoint where the ETag parameter is required. It is here exclusively for the sake of
    232             // 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         }
    236232
    237233        $group_collection = new OD_URL_Metric_Group_Collection(
     
    251247
    252248        $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.
    260251        );
    261252        if ( ! is_string( $post_data['post_content'] ) ) {
  • optimization-detective/trunk/storage/data.php

    r3229869 r3240307  
    2121 * @access private
    2222 *
    23  * @return int Expiration TTL in seconds.
     23 * @return int<0, max> Expiration TTL in seconds.
    2424 */
    2525function od_get_url_metric_freshness_ttl(): int {
     
    3232     * @since 0.1.0
    3333     *
    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 );
    3737
    3838    if ( $freshness_ttl < 0 ) {
     
    5959 * This is used as a cache key for stored URL Metrics.
    6060 *
    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  *
    6361 * @since 0.1.0
    6462 * @access private
     
    118116        $current_url .= ltrim( wp_unslash( $_SERVER['REQUEST_URI'] ), '/' );
    119117    }
     118
     119    // TODO: We should be able to assert that this returns an non-empty-string.
    120120    return esc_url_raw( $current_url );
    121121}
     
    132132 *
    133133 * @param array<string, mixed> $query_vars Normalized query vars.
    134  * @return string Slug.
     134 * @return non-empty-string Slug.
    135135 */
    136136function od_get_url_metrics_slug( array $query_vars ): string {
     
    199199        $queried_object_data['type'] = $queried_object->name;
    200200    }
     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 );
    201212
    202213    $data = array(
     
    231242            ),
    232243        ),
     244        'active_plugins'   => $active_plugins,
    233245        'current_template' => $current_template instanceof WP_Block_Template ? get_object_vars( $current_template ) : $current_template,
    234246    );
     
    258270 * @see od_get_url_metrics_slug()
    259271 *
    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.
    265277 */
    266278function od_get_url_metrics_storage_hmac( string $slug, string $current_etag, string $url, ?int $cache_purge_post_id = null ): string {
    267279    $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;
    269288}
    270289
     
    279298 * @see od_get_url_metrics_slug()
    280299 *
    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.
    286305 * @return bool Whether the HMAC is valid.
    287306 */
     
    361380 * @link https://github.com/WordPress/gutenberg/blob/093d52cbfd3e2c140843d3fb91ad3d03330320a5/packages/base-styles/_breakpoints.scss#L11-L13
    362381 *
    363  * @return int[] Breakpoint max widths, sorted in ascending order.
     382 * @return positive-int[] Breakpoint max widths, sorted in ascending order.
    364383 */
    365384function od_get_breakpoint_max_widths(): array {
     
    369388        static function ( $original_breakpoint ) use ( $function_name ): int {
    370389            $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 ) {
    385391                $breakpoint = 1;
    386392                _doing_it_wrong(
     
    401407         * Filters the breakpoint max widths to group URL Metrics for various viewports.
    402408         *
    403          * A breakpoint must be greater than zero and less than PHP_INT_MAX. This array may be empty in which case there
     409         * A breakpoint must be greater than zero. This array may be empty in which case there
    404410         * are no responsive breakpoints and all URL Metrics are collected in a single group.
    405411         *
    406412         * @since 0.1.0
    407413         *
    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].
    409415         */
    410416        array_map( 'intval', (array) apply_filters( 'od_breakpoint_max_widths', array( 480, 600, 782 ) ) )
     
    426432 * @access private
    427433 *
    428  * @return int Sample size.
     434 * @return int<1, max> Sample size.
    429435 */
    430436function od_get_url_metrics_breakpoint_sample_size(): int {
  • optimization-detective/trunk/storage/rest-api.php

    r3229869 r3240307  
    1616 * Namespace for optimization-detective.
    1717 *
     18 * @since 0.1.0
     19 * @access private
    1820 * @var string
    1921 */
     
    2729 * create a new `od_url_metrics` post, or it will update an existing post if one already exists for the provided slug.
    2830 *
     31 * @since 0.1.0
     32 * @access private
    2933 * @link https://google.aip.dev/136
    3034 * @var string
     
    7377            'pattern'           => '^[0-9a-f]+\z',
    7478            '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 ) ) {
    7680                    return new WP_Error( 'invalid_hmac', __( 'URL Metrics HMAC verification failure.', 'optimization-detective' ) );
    7781                }
     
    215219    }
    216220
     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
    217242    // 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.
    218243    $result = OD_URL_Metrics_Post_Type::store_url_metric(
  • optimization-detective/trunk/types.ts

    r3229869 r3240307  
    3535export interface URLMetricGroupStatus {
    3636    minimumViewportWidth: number;
     37    maximumViewportWidth: number | null;
    3738    complete: boolean;
    3839}
Note: See TracChangeset for help on using the changeset viewer.