Skip to content

Commit c3c2512

Browse files
authored
Merge branch 'trunk' into fix/optimization-detective-non-ascii-chars-in-link-header
2 parents 2d14c6e + e793289 commit c3c2512

File tree

9 files changed

+495
-203
lines changed

9 files changed

+495
-203
lines changed

package-lock.json

Lines changed: 162 additions & 168 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,9 @@
1212
},
1313
"devDependencies": {
1414
"@octokit/rest": "^21.1.0",
15-
"@wordpress/env": "^10.16.0",
16-
"@wordpress/prettier-config": "^4.14.0",
17-
"@wordpress/scripts": "^30.9.0",
15+
"@wordpress/env": "^10.17.0",
16+
"@wordpress/prettier-config": "^4.17.0",
17+
"@wordpress/scripts": "^30.10.0",
1818
"commander": "13.1.0",
1919
"copy-webpack-plugin": "^12.0.2",
2020
"css-minimizer-webpack-plugin": "^7.0.0",

plugins/optimization-detective/detect.js

Lines changed: 101 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -96,33 +96,65 @@ function error( ...message ) {
9696
}
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.
100100
*
101101
* 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()`.
102103
*
103104
* @param {number} viewportWidth - Current viewport width.
104105
* @param {URLMetricGroupStatus[]} urlMetricGroupStatuses - Viewport group statuses.
105-
* @return {boolean} Whether URL Metrics are needed.
106+
* @return {URLMetricGroupStatus} The URL metric group for the viewport width.
106107
*/
107-
function isViewportNeeded( viewportWidth, urlMetricGroupStatuses ) {
108-
if ( viewportWidth === 0 ) {
109-
return false;
110-
}
111-
112-
for ( const {
113-
minimumViewportWidth,
114-
maximumViewportWidth,
115-
complete,
116-
} of urlMetricGroupStatuses ) {
108+
function getGroupForViewportWidth( viewportWidth, urlMetricGroupStatuses ) {
109+
for ( const urlMetricGroupStatus of urlMetricGroupStatuses ) {
117110
if (
118-
viewportWidth > minimumViewportWidth &&
119-
( null === maximumViewportWidth ||
120-
viewportWidth <= maximumViewportWidth )
111+
viewportWidth > urlMetricGroupStatus.minimumViewportWidth &&
112+
( null === urlMetricGroupStatus.maximumViewportWidth ||
113+
viewportWidth <= urlMetricGroupStatus.maximumViewportWidth )
121114
) {
122-
return ! complete;
115+
return urlMetricGroupStatus;
123116
}
124117
}
125-
return false;
118+
throw new Error(
119+
`${ consoleLogPrefix } Unexpectedly unable to locate group for the current viewport width.`
120+
);
121+
}
122+
123+
/**
124+
* Gets the sessionStorage key for keeping track of whether the current client session already submitted a URL Metric.
125+
*
126+
* @param {string} currentETag - Current ETag.
127+
* @param {string} currentUrl - Current URL.
128+
* @param {URLMetricGroupStatus} urlMetricGroupStatus - URL Metric group status.
129+
* @return {Promise<string>} Session storage key.
130+
*/
131+
async function getAlreadySubmittedSessionStorageKey(
132+
currentETag,
133+
currentUrl,
134+
urlMetricGroupStatus
135+
) {
136+
const message = [
137+
currentETag,
138+
currentUrl,
139+
urlMetricGroupStatus.minimumViewportWidth,
140+
urlMetricGroupStatus.maximumViewportWidth || '',
141+
].join( '-' );
142+
143+
/*
144+
* Note that the components are hashed for a couple of reasons:
145+
*
146+
* 1. It results in a consistent length string devoid of any special characters that could cause problems.
147+
* 2. Since the key includes the URL, hashing it avoids potential privacy concerns where the sessionStorage is
148+
* examined to see which URLs the client went to.
149+
*
150+
* The SHA-1 algorithm is chosen since it is the fastest and there is no need for cryptographic security.
151+
*/
152+
const msgBuffer = new TextEncoder().encode( message );
153+
const hashBuffer = await crypto.subtle.digest( 'SHA-1', msgBuffer );
154+
const hashHex = Array.from( new Uint8Array( hashBuffer ) )
155+
.map( ( b ) => b.toString( 16 ).padStart( 2, '0' ) )
156+
.join( '' );
157+
return `odSubmitted-${ hashHex }`;
126158
}
127159

128160
/**
@@ -264,13 +296,15 @@ function extendElementData( xpath, properties ) {
264296
* @param {number} args.maxViewportAspectRatio Maximum aspect ratio allowed for the viewport.
265297
* @param {boolean} args.isDebug Whether to show debug messages.
266298
* @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.
267300
* @param {string} args.currentETag Current ETag.
268301
* @param {string} args.currentUrl Current URL.
269302
* @param {string} args.urlMetricSlug Slug for URL Metric.
270303
* @param {number|null} args.cachePurgePostId Cache purge post ID.
271304
* @param {string} args.urlMetricHMAC HMAC for URL Metric storage.
272305
* @param {URLMetricGroupStatus[]} args.urlMetricGroupStatuses URL Metric group statuses.
273306
* @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.
274308
* @param {string} args.webVitalsLibrarySrc The URL for the web-vitals library.
275309
* @param {CollectionDebugData} [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode.
276310
*/
@@ -280,13 +314,15 @@ export default async function detect( {
280314
isDebug,
281315
extensionModuleUrls,
282316
restApiEndpoint,
317+
restApiNonce,
283318
currentETag,
284319
currentUrl,
285320
urlMetricSlug,
286321
cachePurgePostId,
287322
urlMetricHMAC,
288323
urlMetricGroupStatuses,
289324
storageLockTTL,
325+
freshnessTTL,
290326
webVitalsLibrarySrc,
291327
urlMetricGroupCollection,
292328
} ) {
@@ -308,14 +344,52 @@ export default async function detect( {
308344
);
309345
}
310346

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+
311356
// Abort if the current viewport is not among those which need URL Metrics.
312-
if ( ! isViewportNeeded( win.innerWidth, urlMetricGroupStatuses ) ) {
357+
const urlMetricGroupStatus = getGroupForViewportWidth(
358+
win.innerWidth,
359+
urlMetricGroupStatuses
360+
);
361+
if ( urlMetricGroupStatus.complete ) {
313362
if ( isDebug ) {
314363
log( 'No need for URL Metrics from the current viewport.' );
315364
}
316365
return;
317366
}
318367

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+
}
391+
}
392+
319393
// Abort if the viewport aspect ratio is not in a common range.
320394
const aspectRatio = win.innerWidth / win.innerHeight;
321395
if (
@@ -670,11 +744,20 @@ export default async function detect( {
670744
// because we can't look at the response when sending a beacon.
671745
setStorageLock( getCurrentTime() );
672746

747+
// Remember that the URL Metric was submitted for this URL to avoid having multiple entries submitted by the same client.
748+
sessionStorage.setItem(
749+
alreadySubmittedSessionStorageKey,
750+
String( getCurrentTime() )
751+
);
752+
673753
if ( isDebug ) {
674754
log( 'Sending URL Metric:', urlMetric );
675755
}
676756

677757
const url = new URL( restApiEndpoint );
758+
if ( typeof restApiNonce === 'string' ) {
759+
url.searchParams.set( '_wpnonce', restApiNonce );
760+
}
678761
url.searchParams.set( 'slug', urlMetricSlug );
679762
url.searchParams.set( 'current_etag', currentETag );
680763
if ( typeof cachePurgePostId === 'number' ) {

plugins/optimization-detective/detection.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,8 +138,12 @@ static function ( OD_URL_Metric_Group $group ): array {
138138
iterator_to_array( $group_collection )
139139
),
140140
'storageLockTTL' => OD_Storage_Lock::get_ttl(),
141+
'freshnessTTL' => od_get_url_metric_freshness_ttl(),
141142
'webVitalsLibrarySrc' => $web_vitals_lib_src,
142143
);
144+
if ( is_user_logged_in() ) {
145+
$detect_args['restApiNonce'] = wp_create_nonce( 'wp_rest' );
146+
}
143147
if ( WP_DEBUG ) {
144148
$detect_args['urlMetricGroupCollection'] = $group_collection;
145149
}

plugins/optimization-detective/docs/hooks.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -102,16 +102,20 @@ add_filter( 'od_url_metrics_breakpoint_sample_size', function (): int {
102102
} );
103103
```
104104

105-
### Filter: `od_url_metric_storage_lock_ttl` (default: 1 minute in seconds)
105+
### Filter: `od_url_metric_storage_lock_ttl` (default: 60 seconds, except 0 for authorized logged-in users)
106106

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:
107+
Filters how long the current IP is locked from submitting another URL metric storage REST API request.
108+
109+
Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable locking when a user is logged-in with code like the following:
108110

109111
```php
110112
add_filter( 'od_metrics_storage_lock_ttl', function ( int $ttl ): int {
111113
return is_user_logged_in() ? 0 : $ttl;
112114
} );
113115
```
114116

117+
By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter.
118+
115119
During development this is useful to set to zero so you can quickly collect new URL Metrics by reloading the page without having to wait for the storage lock to release:
116120

117121
```php

plugins/optimization-detective/hooks.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
add_action( 'init', 'od_initialize_extensions', PHP_INT_MAX );
1919
add_filter( 'template_include', 'od_buffer_output', PHP_INT_MAX );
2020
OD_URL_Metrics_Post_Type::add_hooks();
21+
OD_Storage_Lock::add_hooks();
2122
add_action( 'wp', 'od_maybe_add_template_output_buffer_filter' );
2223
add_action( 'wp_head', 'od_render_generator_meta_tag' );
2324
add_filter( 'site_status_tests', 'od_add_rest_api_availability_test' );

plugins/optimization-detective/storage/class-od-storage-lock.php

Lines changed: 48 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,44 @@
2020
*/
2121
final class OD_Storage_Lock {
2222

23+
/**
24+
* Capability for being able to store a URL Metric now.
25+
*
26+
* @since n.e.x.t
27+
* @access private
28+
* @var string
29+
*/
30+
const STORE_URL_METRIC_NOW_CAPABILITY = 'od_store_url_metric_now';
31+
32+
/**
33+
* Adds hooks.
34+
*
35+
* @since n.e.x.t
36+
* @access private
37+
*/
38+
public static function add_hooks(): void {
39+
add_filter( 'user_has_cap', array( __CLASS__, 'filter_user_has_cap' ) );
40+
}
41+
42+
/**
43+
* Filters `user_has_cap` to grant the `od_store_url_metric_now` capability to users who can `manage_options` by default.
44+
*
45+
* @since n.e.x.t
46+
* @access private
47+
*
48+
* @param array<string, bool>|mixed $allcaps Capability names mapped to boolean values for whether the user has that capability.
49+
* @return array<string, bool> Capability names mapped to boolean values for whether the user has that capability.
50+
*/
51+
public static function filter_user_has_cap( $allcaps ): array {
52+
if ( ! is_array( $allcaps ) ) {
53+
$allcaps = array();
54+
}
55+
if ( isset( $allcaps['manage_options'] ) ) {
56+
$allcaps['od_store_url_metric_now'] = $allcaps['manage_options'];
57+
}
58+
return $allcaps;
59+
}
60+
2361
/**
2462
* Gets the TTL (in seconds) for the URL Metric storage lock.
2563
*
@@ -29,22 +67,28 @@ final class OD_Storage_Lock {
2967
* @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.
3068
*/
3169
public static function get_ttl(): int {
70+
$ttl = current_user_can( self::STORE_URL_METRIC_NOW_CAPABILITY ) ? 0 : MINUTE_IN_SECONDS;
3271

3372
/**
34-
* Filters how long a given IP is locked from submitting another metric-storage REST API request.
73+
* Filters how long the current IP is locked from submitting another URL metric storage REST API request.
3574
*
36-
* Filtering the TTL to zero will disable any metric storage locking. This is useful, for example, to disable
75+
* Filtering the TTL to zero will disable any URL Metric storage locking. This is useful, for example, to disable
3776
* locking when a user is logged-in with code like the following:
3877
*
3978
* add_filter( 'od_metrics_storage_lock_ttl', static function ( int $ttl ): int {
4079
* return is_user_logged_in() ? 0 : $ttl;
4180
* } );
4281
*
82+
* By default, the TTL is zero (0) for authorized users and sixty (60) for everyone else. Whether the current
83+
* user is authorized is determined by whether the user has the `od_store_url_metric_now` capability. This
84+
* custom capability by default maps to the `manage_options` primitive capability via the `user_has_cap` filter.
85+
*
4386
* @since 0.1.0
87+
* @since 1.0.0 This now defaults to zero (0) for authorized users.
4488
*
45-
* @param int $ttl TTL.
89+
* @param int $ttl TTL. Defaults to 60, except zero (0) for authorized users.
4690
*/
47-
$ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', MINUTE_IN_SECONDS );
91+
$ttl = (int) apply_filters( 'od_url_metric_storage_lock_ttl', $ttl );
4892
return max( 0, $ttl );
4993
}
5094

0 commit comments

Comments
 (0)