@@ -96,33 +96,65 @@ function error( ...message ) {
96
96
}
97
97
98
98
/**
99
- * Checks whether the URL Metric(s) for the provided viewport width is needed .
99
+ * Gets the status for the URL Metric group for the provided viewport width.
100
100
*
101
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()`.
102
103
*
103
104
* @param {number } viewportWidth - Current viewport width.
104
105
* @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 .
106
107
*/
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 ) {
117
110
if (
118
- viewportWidth > minimumViewportWidth &&
119
- ( null === maximumViewportWidth ||
120
- viewportWidth <= maximumViewportWidth )
111
+ viewportWidth > urlMetricGroupStatus . minimumViewportWidth &&
112
+ ( null === urlMetricGroupStatus . maximumViewportWidth ||
113
+ viewportWidth <= urlMetricGroupStatus . maximumViewportWidth )
121
114
) {
122
- return ! complete ;
115
+ return urlMetricGroupStatus ;
123
116
}
124
117
}
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 } ` ;
126
158
}
127
159
128
160
/**
@@ -264,13 +296,15 @@ function extendElementData( xpath, properties ) {
264
296
* @param {number } args.maxViewportAspectRatio Maximum aspect ratio allowed for the viewport.
265
297
* @param {boolean } args.isDebug Whether to show debug messages.
266
298
* @param {string } args.restApiEndpoint URL for where to send the detection data.
299
+ * @param {string } [args.restApiNonce] Nonce for the REST API when the user is logged-in.
267
300
* @param {string } args.currentETag Current ETag.
268
301
* @param {string } args.currentUrl Current URL.
269
302
* @param {string } args.urlMetricSlug Slug for URL Metric.
270
303
* @param {number|null } args.cachePurgePostId Cache purge post ID.
271
304
* @param {string } args.urlMetricHMAC HMAC for URL Metric storage.
272
305
* @param {URLMetricGroupStatus[] } args.urlMetricGroupStatuses URL Metric group statuses.
273
306
* @param {number } args.storageLockTTL The TTL (in seconds) for the URL Metric storage lock.
307
+ * @param {number } args.freshnessTTL The freshness age (TTL) for a given URL Metric.
274
308
* @param {string } args.webVitalsLibrarySrc The URL for the web-vitals library.
275
309
* @param {CollectionDebugData } [args.urlMetricGroupCollection] URL Metric group collection, when in debug mode.
276
310
*/
@@ -280,13 +314,15 @@ export default async function detect( {
280
314
isDebug,
281
315
extensionModuleUrls,
282
316
restApiEndpoint,
317
+ restApiNonce,
283
318
currentETag,
284
319
currentUrl,
285
320
urlMetricSlug,
286
321
cachePurgePostId,
287
322
urlMetricHMAC,
288
323
urlMetricGroupStatuses,
289
324
storageLockTTL,
325
+ freshnessTTL,
290
326
webVitalsLibrarySrc,
291
327
urlMetricGroupCollection,
292
328
} ) {
@@ -308,14 +344,52 @@ export default async function detect( {
308
344
) ;
309
345
}
310
346
347
+ if ( win . innerWidth === 0 || win . innerHeight === 0 ) {
348
+ if ( isDebug ) {
349
+ log (
350
+ 'Window must have non-zero dimensions for URL Metric collection.'
351
+ ) ;
352
+ }
353
+ return ;
354
+ }
355
+
311
356
// 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 ) {
313
362
if ( isDebug ) {
314
363
log ( 'No need for URL Metrics from the current viewport.' ) ;
315
364
}
316
365
return ;
317
366
}
318
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
+ }
391
+ }
392
+
319
393
// Abort if the viewport aspect ratio is not in a common range.
320
394
const aspectRatio = win . innerWidth / win . innerHeight ;
321
395
if (
@@ -670,11 +744,20 @@ export default async function detect( {
670
744
// because we can't look at the response when sending a beacon.
671
745
setStorageLock ( getCurrentTime ( ) ) ;
672
746
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
+
673
753
if ( isDebug ) {
674
754
log ( 'Sending URL Metric:' , urlMetric ) ;
675
755
}
676
756
677
757
const url = new URL ( restApiEndpoint ) ;
758
+ if ( typeof restApiNonce === 'string' ) {
759
+ url . searchParams . set ( '_wpnonce' , restApiNonce ) ;
760
+ }
678
761
url . searchParams . set ( 'slug' , urlMetricSlug ) ;
679
762
url . searchParams . set ( 'current_etag' , currentETag ) ;
680
763
if ( typeof cachePurgePostId === 'number' ) {
0 commit comments