Skip to content
Open
Show file tree
Hide file tree
Changes from 56 commits
Commits
Show all changes
78 commits
Select commit Hold shift + click to select a range
57f9958
Add settings page for Optimization Detective
b1ink0 Jan 23, 2025
71cd706
Add initial admin script and UI elements for URL priming
b1ink0 Jan 27, 2025
da1d275
Implement batch URL priming functionality with REST API endpoint
b1ink0 Jan 27, 2025
bfed250
Include breakpoints in URL processing
b1ink0 Jan 27, 2025
6ec82e3
Add debug mode and better dynamic height generation for breakpoints
b1ink0 Jan 28, 2025
af0172e
Add verification token auth for protecting REST API, Add message pass…
b1ink0 Jan 29, 2025
bff9973
Refactor to improve control flow and add support for pause and resume…
b1ink0 Jan 29, 2025
c155fc1
Move batch URL fetching logic to helper function
b1ink0 Jan 30, 2025
7640ef7
Add support to filter URL whose metrics are already populated
b1ink0 Jan 30, 2025
f05f6c0
Add support to filter breakpoints whose metrics are already populated
b1ink0 Jan 31, 2025
0631daf
Refactor by moving filtering batch URls and computing standard brekpo…
b1ink0 Jan 31, 2025
5d93fdc
Fix sending empty batch URLs caused by filtering the batch URLs
b1ink0 Jan 31, 2025
3118fb3
Save last batch cursor in options table to improve performance
b1ink0 Jan 31, 2025
d8cfa02
Improve verification token handling by using a transient check before…
b1ink0 Feb 3, 2025
8879f60
Add beforeunload event listener to prevent page navigation during pro…
b1ink0 Feb 3, 2025
72b18fd
Add new REST API routes for URL metrics breakpoints and verification …
b1ink0 Feb 3, 2025
f5bba48
Add block editor support for priming URL metrics
b1ink0 Feb 3, 2025
b0422eb
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Feb 4, 2025
db1fd81
Add missing since and access annotations, Add new JavaScripts to webp…
b1ink0 Feb 4, 2025
63564c4
Remove usage of fetch and Fix URL metrics not priming due to usage of…
b1ink0 Feb 4, 2025
4fd36b6
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Feb 4, 2025
82a16b5
Fix detect.js getting stuck on 'onLCP' due to IFRAME being offscreen …
b1ink0 Feb 5, 2025
a50f23e
Improve responsiveness of IFRAME in debug mode
b1ink0 Feb 5, 2025
9201c86
Merge branch 'WordPress:trunk' into add/url-metrics-priming
b1ink0 Feb 6, 2025
163d24e
Skip check of already submitted URL metrics
b1ink0 Feb 7, 2025
57d77ea
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Feb 7, 2025
2bc9f49
Fix `IFRAME` positioning
b1ink0 Feb 7, 2025
d17dede
Add URL priming functionality for classic editor
b1ink0 Feb 7, 2025
97b888f
Refactor to prime URL metrics only when the post is saved
b1ink0 Feb 10, 2025
931861b
Move URL metrics success message to the end of the detection process
b1ink0 Feb 10, 2025
0a7c1f1
Add instructions and important information section to settings page, …
b1ink0 Feb 11, 2025
969f6b5
Pause URL metrics processing when tab visibility chages, Add abort fu…
b1ink0 Feb 11, 2025
c1e245d
Implement task processing with abort functionality and visibility han…
b1ink0 Feb 12, 2025
b7eeef7
Add settings link to plugin action links
b1ink0 Feb 13, 2025
41f5085
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Feb 14, 2025
42bfb98
Add minification support for classic editor JavaScript file
b1ink0 Feb 14, 2025
7487ae8
Add function to disable admin-based URL priming feature when reached …
b1ink0 Feb 18, 2025
49f9eab
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Mar 4, 2025
fed3012
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Mar 14, 2025
591a013
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Mar 18, 2025
17e24c4
Refactor REST API endpoints to use class
b1ink0 Mar 18, 2025
32f1e9e
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Mar 31, 2025
53cc462
Refactor large URL metrics processing function into multiple smaller …
b1ink0 Mar 31, 2025
f9fd604
Refactor to avoid creation of new `AbortController` for each task
b1ink0 Apr 1, 2025
8de014a
Add URL metrics priming CLI
b1ink0 Apr 3, 2025
ce69218
Improve task processing with `AbortController` and improve logging
b1ink0 Apr 3, 2025
7908b89
Refactor to use custom event for communication and improve spinner me…
b1ink0 Apr 3, 2025
3df2f36
Refactor to use global varible to pass verification token to page
b1ink0 Apr 3, 2025
14f8be3
Add environment checks
b1ink0 Apr 4, 2025
fdcb05f
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Apr 11, 2025
a2ebf17
Update js-lint workflow to install npm packages for optimization-dete…
b1ink0 Apr 11, 2025
3a795e9
Improve communication of response of URL Metric sending request
b1ink0 Apr 11, 2025
4f93e14
Refactor to use URL fragment for passing verification token
b1ink0 Apr 17, 2025
ff6ad0f
Add source property to URL Metric schema
b1ink0 Apr 17, 2025
ec5dcd7
Add auto scrolling for URL Metric priming mode
b1ink0 Apr 21, 2025
2550f96
Add function to force URL Metrics compression when in priming mode
b1ink0 Apr 22, 2025
5e64b39
Refactor to make `source` property readonly
b1ink0 Apr 23, 2025
7d42cac
Fix failing test because of new `source` property
b1ink0 Apr 24, 2025
d21ac27
Improve logging for failed tasks, Improve DOC comment of variables
b1ink0 Apr 25, 2025
3b41a12
Standardize functions and improve event handling in priming helper sc…
b1ink0 Apr 25, 2025
d6a07b1
Refactor URL metrics priming API and scripts for consistency and impr…
b1ink0 May 2, 2025
b201dbe
Improve URL metrics priming UI and CLI parameter handling
b1ink0 May 2, 2025
5c7dca6
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Jun 2, 2025
d38e5ca
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Jun 9, 2025
ab9ce29
Add missing parameter type
b1ink0 Jul 31, 2025
26dda1d
Add TypeScript types for Priming CLI
b1ink0 Jul 31, 2025
aa7391b
Add priming mode source tracking
b1ink0 Jul 31, 2025
619dd8d
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Jul 31, 2025
5335d2a
Fix missing sanitation of nonce for classic editor post update
b1ink0 Jul 31, 2025
661dcf1
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Aug 29, 2025
c934b86
Refactor URL metrics priming functions and variables for consistency …
b1ink0 Aug 29, 2025
78c838d
Improve priming mode verification token handling with auto-refresh
b1ink0 Aug 29, 2025
d73842a
Cache sitemap URL count for priming mode
b1ink0 Sep 1, 2025
5b7a26c
Replace computed breakpoints with real device viewports
b1ink0 Sep 9, 2025
fd94d9d
Fix failing tests caused by the addition of a new breakpoint max width
b1ink0 Sep 10, 2025
4943768
Fix missed failing tests caused by the addition of a new breakpoint m…
b1ink0 Sep 12, 2025
7afe854
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Oct 16, 2025
372690f
Merge branch 'trunk' into add/url-metrics-priming
b1ink0 Dec 23, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .github/workflows/js-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ jobs:
node-version-file: '.nvmrc'
cache: npm
- name: npm install
run: npm ci
run: |
npm ci
npm ci --prefix plugins/optimization-detective/priming-cli
- name: JS Lint
run: npm run lint-js
- name: TypeScript compile
Expand Down
24 changes: 23 additions & 1 deletion plugins/optimization-detective/class-od-url-metric.php
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
* url: non-empty-string,
* timestamp: float,
* viewport: ViewportRect,
* elements: ElementData[]
* elements: ElementData[],
* source?: non-empty-string,
* }
* @phpstan-type JSONSchema array{
* type: string|string[],
Expand Down Expand Up @@ -310,6 +311,16 @@ public static function get_json_schema(): array {
'additionalProperties' => true,
),
),
'source' => array(
'description' => __( 'The source of the URL Metric.', 'optimization-detective' ),
'type' => 'string',
'required' => false,
'enum' => array(
'visitor',
'user',
'synthetic',
),
),
Comment on lines +314 to +324
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Should also be readonly, populated in the endpoint callback here:

// Now supply the readonly args which were omitted from the REST API params due to being `readonly`.
'timestamp' => microtime( true ),
'uuid' => wp_generate_uuid4(),
'etag' => $request->get_param( 'current_etag' ),

The value can be synthetic if the prime_url_metrics_verification_token param is present, or else user if is_user_logged_in(). Otherwise, it can be visitor.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Done in 5e64b39

),
// Additional root properties may be added to the schema via the od_url_metric_schema_root_additional_properties filter.
// Therefore, additionalProperties is set to true so that additional properties defined in the extended schema may persist
Expand Down Expand Up @@ -526,6 +537,17 @@ function ( array $element ): OD_Element {
return $this->elements;
}

/**
* Gets the source of the URL Metric.
*
* @since n.e.x.t
*
* @return non-empty-string|null Source.
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The literal values could be used instead:

Suggested change
* @return non-empty-string|null Source.
* @return 'visitor'|'user'|'synthetic'|null Source.

*/
public function get_source(): ?string {
return $this->data['source'] ?? null;
}

/**
* Specifies data which should be serialized to JSON.
*
Expand Down
194 changes: 179 additions & 15 deletions plugins/optimization-detective/detect.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ const storageLockTimeSessionKey = 'odStorageLockTime';
*/
const compressionDebounceWaitDuration = 1000;

/**
* Verification token for skipping the storage lock check while priming URL Metrics.
*
* @see {detect}
* @type {?string}
*/
let odPrimeUrlMetricsVerificationToken = null;

/**
* Checks whether storage is locked.
*
Expand All @@ -73,6 +81,10 @@ const compressionDebounceWaitDuration = 1000;
* @return {boolean} Whether storage is locked.
*/
function isStorageLocked( currentTime, storageLockTTL ) {
if ( odPrimeUrlMetricsVerificationToken ) {
return false;
}

if ( storageLockTTL === 0 ) {
return false;
}
Expand Down Expand Up @@ -503,6 +515,116 @@ function debounceCompressUrlMetric() {
}, compressionDebounceWaitDuration );
}

/**
* Forces immediate compression of the URL Metric, bypassing debounce and idle callbacks.
*
* @return {Promise<void>} A promise that resolves when the compression is complete.
*/
async function forceCompressUrlMetric() {
if ( ! compressionEnabled ) {
return null;
}
if ( null !== recompressionTimeout ) {
clearTimeout( recompressionTimeout );
recompressionTimeout = null;
}
if (
null !== idleCallbackHandle &&
typeof cancelIdleCallback === 'function'
) {
cancelIdleCallback( idleCallbackHandle );
idleCallbackHandle = null;
}

try {
compressedPayload = await compress( JSON.stringify( urlMetric ) );
} catch ( err ) {
const { error } = createLogger( false, consoleLogPrefix );
error(
'Failed to compress URL Metric falling back to sending uncompressed data:',
err
);
compressionEnabled = false;
}
}

/**
* Notifies about the URL Metric request status.
*
* @param {Object} status - The status details.
* @param {boolean} status.success - Indicates if the request succeeded.
* @param {string} [status.error] - An error message if the request failed.
* @param {Object} [options] - Options for where to dispatch the message.
* @param {boolean} [options.toParent=true] - Whether to send the message to the parent window.
* @param {boolean} [options.toLocal=true] - Whether to dispatch a custom event locally.
*/
function notifyStatus( status, options = { toParent: true, toLocal: true } ) {
const message = {
type: 'OD_PRIME_URL_METRICS_REQUEST_STATUS',
success: status.success,
...( status.error && { error: status.error } ),
};

// This will be used when URL metrics are primed using a IFRAME.
if ( options.toParent && window.parent && window.parent !== window ) {
window.parent.postMessage( message, '*' );
}

// This will be used when URL metrics are primed using Puppeteer script.
if ( options.toLocal ) {
document.dispatchEvent(
new CustomEvent( message.type, {
detail: { ...status },
} )
);
}
}

/**
* Scrolls to the bottom of the page.
*
* @return {Promise<void>} A promise that resolves when the scroll is complete.
*/
async function scrollToBottomOfPage() {
return new Promise( ( resolve ) => {
const viewportHeight = window.innerHeight;
const maxScrollAttempts = 20;
const maxHeightChangeAmount = viewportHeight * 0.5;
const maxHeightChangeAttempts = 3;

let scrollAttempts = 0;
let heightChangeAttempts = 0;
let lastScrollPosition = 0;
let lastHeight = document.documentElement.scrollHeight;

function scroll() {
const newHeight = document.documentElement.scrollHeight;
if (
window.innerHeight + window.scrollY >= newHeight - 10 ||
scrollAttempts >= maxScrollAttempts ||
heightChangeAttempts >= maxHeightChangeAttempts
) {
resolve();
return;
}

lastScrollPosition += viewportHeight;
window.scrollTo( { top: lastScrollPosition, behavior: 'smooth' } );

const heightChange = newHeight - lastHeight;
if ( heightChange >= maxHeightChangeAmount ) {
lastHeight = newHeight;
heightChangeAttempts++;
}

scrollAttempts++;
setTimeout( scroll, 300 );
}

scroll();
} );
}

/**
* @typedef {{timestamp: number, creationDate: Date}} UrlMetricDebugData
* @typedef {{groups: Array<{url_metrics: Array<UrlMetricDebugData>}>}} CollectionDebugData
Expand Down Expand Up @@ -595,6 +717,15 @@ export default async function detect( {
return;
}

// Retrieve verification token from the URL hash for priming URL Metrics.
// Presence of the token indicates that the URL Metric is being primed
// through the Puppeteer script or WordPress admin dashboard.
if ( '' !== window.location.hash ) {
odPrimeUrlMetricsVerificationToken = new URLSearchParams(
window.location.hash.slice( 1 )
).get( 'odPrimeUrlMetricsVerificationToken' );
}

// Abort if the client already submitted a URL Metric for this URL and viewport group.
const alreadySubmittedSessionStorageKey =
await getAlreadySubmittedSessionStorageKey(
Expand All @@ -604,6 +735,7 @@ export default async function detect( {
logger
);
if (
! odPrimeUrlMetricsVerificationToken &&
null !== alreadySubmittedSessionStorageKey &&
alreadySubmittedSessionStorageKey in sessionStorage
) {
Expand Down Expand Up @@ -787,8 +919,13 @@ export default async function detect( {
height: win.innerHeight,
},
elements: [],
source: restApiNonce ? 'user' : 'visitor',
};

if ( odPrimeUrlMetricsVerificationToken ) {
urlMetric.source = 'synthetic';
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Populating the source should probably be done in the endpoint itself. It should be a readonly param.


const lcpMetric = lcpMetricCandidates.at( -1 );

// Populate the elements in the URL Metric.
Expand Down Expand Up @@ -921,19 +1058,25 @@ export default async function detect( {
debounceCompressUrlMetric();

// Wait for the page to be hidden.
await new Promise( ( resolve ) => {
win.addEventListener( 'pagehide', resolve, { once: true } );
win.addEventListener( 'pageswap', resolve, { once: true } );
doc.addEventListener(
'visibilitychange',
() => {
if ( document.visibilityState === 'hidden' ) {
// TODO: This will fire even when switching tabs.
resolve();
}
},
{ once: true }
);
await new Promise( async ( resolve ) => {
if ( ! odPrimeUrlMetricsVerificationToken ) {
win.addEventListener( 'pagehide', resolve, { once: true } );
win.addEventListener( 'pageswap', resolve, { once: true } );
doc.addEventListener(
'visibilitychange',
() => {
if ( document.visibilityState === 'hidden' ) {
// TODO: This will fire even when switching tabs.
resolve();
}
},
{ once: true }
);
} else {
await scrollToBottomOfPage();
await forceCompressUrlMetric();
resolve();
}
} );

// Only proceed with submitting the URL Metric if viewport stayed the same size. Changing the viewport size (e.g. due
Expand Down Expand Up @@ -1092,6 +1235,12 @@ export default async function detect( {
);
}
url.searchParams.set( 'hmac', urlMetricHMAC );
if ( odPrimeUrlMetricsVerificationToken ) {
url.searchParams.set(
'prime_url_metrics_verification_token',
odPrimeUrlMetricsVerificationToken
);
}
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Authentication for REST API

  • WP Nonce Limitation: The default WordPress (WP) nonce does not function correctly when generated for the parent page and then passed to an iframe for REST API requests.

  • Custom Token Authentication: To address this, I have added a custom token-based authentication mechanism. This generates a time-limited token used to authenticate REST API requests made via the iframe.

In #1835 PR, WP nonces are introduced for REST API requests for logged-in users. This may allow us to eliminate the custom token authentication if URL metrics are collected exclusively from logged-in users.


const headers = {
'Content-Type': 'application/json',
Expand All @@ -1104,7 +1253,22 @@ export default async function detect( {
method: 'POST',
body: payloadBlob,
headers,
keepalive: true, // This makes fetch() behave the same as navigator.sendBeacon().
keepalive: odPrimeUrlMetricsVerificationToken ? false : true, // Setting keepalive to true makes fetch() behave the same as navigator.sendBeacon().
} );
await fetch( request );

if ( ! odPrimeUrlMetricsVerificationToken ) {
await fetch( request );
} else {
try {
const response = await fetch( request );
if ( ! response.ok ) {
throw new Error(
`Failed to send URL Metric. Status: ${ response.status }`
);
}
notifyStatus( { success: true } );
} catch ( err ) {
notifyStatus( { success: false, error: err.message } );
}
}
}
25 changes: 25 additions & 0 deletions plugins/optimization-detective/detection.php
Original file line number Diff line number Diff line change
Expand Up @@ -236,6 +236,31 @@ function_exists( 'gzdecode' ) &&
return $result;
}

/**
* Registers the REST API endpoint for priming URL Metrics.
*
* @since n.e.x.t
* @access private
*/
function od_register_rest_url_metric_priming_endpoint(): void {
$endpoint_controller = new OD_REST_URL_Metrics_Priming_Endpoint();
register_rest_route(
OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE,
$endpoint_controller::PRIME_URLS_ROUTE,
$endpoint_controller->get_registration_args_prime_urls()
);
register_rest_route(
OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE,
$endpoint_controller::PRIME_URLS_BREAKPOINTS_ROUTE,
$endpoint_controller->get_registration_args_prime_urls_breakpoints()
);
register_rest_route(
OD_REST_URL_Metrics_Store_Endpoint::ROUTE_NAMESPACE,
$endpoint_controller::PRIME_URLS_VERIFICATION_TOKEN_ROUTE,
$endpoint_controller->get_registration_args_prime_urls_verification_token()
);
}

/**
* Triggers post update actions for page caches to invalidate their caches related to the supplied cache purge post ID.
*
Expand Down
Loading
Loading