From 3f3d87c0a243375876dcdd16f9a22697c2bfff9a Mon Sep 17 00:00:00 2001 From: Sarthak Jaiswal Date: Fri, 2 Oct 2026 15:55:53 +0530 Subject: [PATCH] Harden Optimization Detective XPath data against HTML-mangling plugins Replace the data-od-xpath attribute, which embeds raw XPath text directly in HTML, with an auto-incremented data-od-id attribute. The id-to-xpath mapping is shipped once as a gzip-compressed, base64-encoded JSON SCRIPT tag (falling back to plain JSON when gzip is unavailable) and decoded by detect.js at initialization. This prevents third-party "HTML minifier" plugins that use naive regular expressions to strip JS/CSS comments from mistaking an XPath's /* for the start of a comment and corrupting the page, while also shrinking the HTML payload and avoiding search engines treating XPath strings as crawlable URLs. Embed Optimizer's detect.js, which previously read data-od-xpath directly off the DOM, now resolves XPaths through a new getElementXPath() function exposed to extensions via the initialize()/finalize() args. Fixes #1947 --- plugins/embed-optimizer/detect.js | 11 +- .../nested-figure-embed/expected.html | 11 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../too-many-bookmarks/expected.html | 7 +- .../expected.html | 5 +- .../expected.html | 7 +- .../expected.html | 5 +- .../expected.html | 13 +- .../expected.html | 7 +- .../expected.html | 7 +- .../expected.html | 7 +- .../expected.html | 5 +- .../expected.html | 13 +- .../expected.html | 13 +- .../expected.html | 11 +- .../test-cases/no-url-metrics/expected.html | 7 +- .../expected.html | 7 +- .../expected.html | 5 +- .../expected.html | 7 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 5 +- .../expected.html | 7 +- .../expected.html | 7 +- .../expected.html | 7 +- plugins/optimization-detective/detect.js | 124 +- plugins/optimization-detective/detection.php | 32 +- .../optimization-detective/optimization.php | 19 +- ...ss-optimization-detective-test-helpers.php | 14 + .../tests/test-cases/admin-bar/expected.html | 5 +- .../test-cases/many-images/expected.html | 2003 +++++++++-------- .../test-cases/no-url-metrics/expected.html | 5 +- .../test-cases/preload-link/expected.html | 5 +- .../test-cases/stray-closing-p/expected.html | 7 +- .../test-cases/tag-track-opt-in/expected.html | 9 +- .../tests/test-cases/video/expected.html | 5 +- .../test-cases/xhtml-response/expected.html | 5 +- .../tests/test-detection.php | 57 +- plugins/optimization-detective/types.ts | 3 + 45 files changed, 1379 insertions(+), 1133 deletions(-) diff --git a/plugins/embed-optimizer/detect.js b/plugins/embed-optimizer/detect.js index f408ef551a..5803ef69b1 100644 --- a/plugins/embed-optimizer/detect.js +++ b/plugins/embed-optimizer/detect.js @@ -15,6 +15,7 @@ export const name = 'Embed Optimizer'; * @typedef {import("../optimization-detective/types.ts").InitializeArgs} InitializeArgs * @typedef {import("../optimization-detective/types.ts").GetElementDataFunction} GetElementDataFunction * @typedef {import("../optimization-detective/types.ts").ExtendElementDataFunction} ExtendElementDataFunction + * @typedef {import("../optimization-detective/types.ts").GetElementXPathFunction} GetElementXPathFunction * @typedef {import("../optimization-detective/types.ts").ExtendedElementData} ExtendedElementData * @typedef {import("../optimization-detective/types.ts").LogFunction} LogFunction */ @@ -30,10 +31,11 @@ export async function initialize( { error, getElementData, extendElementData, + getElementXPath, } ) { /** @type NodeListOf */ const embedWrappers = document.querySelectorAll( - '.wp-block-embed > .wp-block-embed__wrapper[data-od-xpath]' + '.wp-block-embed > .wp-block-embed__wrapper[data-od-id]' ); for ( /** @type {HTMLElement} */ const embedWrapper of embedWrappers ) { @@ -41,6 +43,7 @@ export async function initialize( { embedWrapper, extendElementData, getElementData, + getElementXPath, log, error ); @@ -53,6 +56,7 @@ export async function initialize( { * @param {HTMLDivElement} embedWrapper - Embed wrapper DIV. * @param {ExtendElementDataFunction} extendElementData - Function to extend element data with. * @param {GetElementDataFunction} getElementData - Function to get element data. + * @param {GetElementXPathFunction} getElementXPath - Function to get the XPath for an element. * @param {LogFunction} log - The function to call with log messages. * @param {LogFunction} error - The function to call with error messages. */ @@ -60,12 +64,13 @@ function monitorEmbedWrapperForResizes( embedWrapper, extendElementData, getElementData, + getElementXPath, log, error ) { - const xpath = embedWrapper.dataset.odXpath; + const xpath = getElementXPath( embedWrapper ); if ( ! xpath ) { - throw new Error( 'Embed wrapper missing data-od-xpath attribute.' ); + throw new Error( 'Embed wrapper missing data-od-id attribute.' ); } const observer = new ResizeObserver( ( entries ) => { const [ entry ] = entries; diff --git a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed/expected.html b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed/expected.html index 6e1a53640e..0e6b5e7ef7 100644 --- a/plugins/embed-optimizer/tests/test-cases/nested-figure-embed/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/nested-figure-embed/expected.html @@ -23,15 +23,15 @@
-
- +
+
-
+

So I heard you like FIGURE?

- +
Tagline from Figurine embed.
@@ -39,7 +39,8 @@
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script/expected.html b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script/expected.html index 95962b1e76..8e3430add2 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-spotify-embed-outside-viewport-with-subsequent-script/expected.html @@ -15,13 +15,14 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport/expected.html b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport/expected.html index 6c773e3b44..b62a959643 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-inside-viewport/expected.html @@ -19,13 +19,14 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile/expected.html b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile/expected.html index 0c77a03539..486cdef3d6 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport-on-mobile/expected.html @@ -19,13 +19,14 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport/expected.html b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport/expected.html index d8b1e77a3e..dda6e05c23 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-wordpress-tv-embed-outside-viewport/expected.html @@ -15,14 +15,15 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport-with-only-mobile-url-metrics/expected.html b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport-with-only-mobile-url-metrics/expected.html index 150b742e78..a873ce1e90 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport-with-only-mobile-url-metrics/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-inside-viewport-with-only-mobile-url-metrics/expected.html @@ -14,12 +14,13 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-with-only-mobile-url-metrics/expected.html b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-with-only-mobile-url-metrics/expected.html index f3ecda5b30..636587860a 100644 --- a/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-with-only-mobile-url-metrics/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/single-youtube-embed-outside-viewport-with-only-mobile-url-metrics/expected.html @@ -12,12 +12,13 @@
-
+
- + + diff --git a/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks/expected.html b/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks/expected.html index 6b7caac8bb..f5d3287dd1 100644 --- a/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks/expected.html +++ b/plugins/embed-optimizer/tests/test-cases/too-many-bookmarks/expected.html @@ -5,16 +5,17 @@ - +
-
+
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/expected.html b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/expected.html index f186dfa4db..42f1e5ff49 100644 --- a/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/background-image-outside-viewport-with-desktop-metrics-missing/expected.html @@ -8,9 +8,10 @@

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

-
This is so background!
+
This is so background!
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-fully-incomplete-sample-data/expected.html b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-fully-incomplete-sample-data/expected.html index 8c66536fb8..33842460c3 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-fully-incomplete-sample-data/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-fully-incomplete-sample-data/expected.html @@ -8,10 +8,11 @@
- Foo - Bar + Foo + Bar
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-old-xpath-format/expected.html b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-old-xpath-format/expected.html index e8d0cab424..b36d12f09a 100644 --- a/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-old-xpath-format/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/common-lcp-image-with-old-xpath-format/expected.html @@ -7,9 +7,10 @@
- Foo + Foo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints/expected.html index 95efdc2823..ca447db0f8 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-all-breakpoints/expected.html @@ -12,13 +12,14 @@
- Mobile Logo - Phablet Logo - Tablet Logo - Desktop Logo - Desktop Logo + Mobile Logo + Phablet Logo + Tablet Logo + Desktop Logo + Desktop Logo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group/expected.html b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group/expected.html index c891be4e05..ad76f40fcc 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-non-consecutive-viewport-groups-with-missing-data-for-middle-group/expected.html @@ -8,10 +8,11 @@
- Mobile Logo - Desktop Logo + Mobile Logo + Desktop Logo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints-and-one-is-stale/expected.html b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints-and-one-is-stale/expected.html index c2be56eee1..458a0c075a 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints-and-one-is-stale/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints-and-one-is-stale/expected.html @@ -8,11 +8,12 @@
- Mobile Logo + Mobile Logo

New paragraph since URL Metrics were captured!

- Desktop Logo + Desktop Logo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints/expected.html index 9d7d91c9a9..c13efb449c 100644 --- a/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/different-lcp-elements-for-two-non-consecutive-breakpoints/expected.html @@ -9,10 +9,11 @@
- Mobile Logo - Desktop Logo + Mobile Logo + Desktop Logo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups/expected.html b/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups/expected.html index 37b9265ab4..7733a11248 100644 --- a/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/fetch-priority-high-on-lcp-image-common-on-mobile-and-desktop-with-url-metrics-missing-in-other-groups/expected.html @@ -9,9 +9,10 @@
- Foo + Foo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html index 7a1458bba1..e8649604b8 100644 --- a/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/multiple-videos-on-all-breakpoints/expected.html @@ -8,14 +8,15 @@
- - - - - + + + + +
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/multiple-videos-with-desktop-metrics-missing/expected.html b/plugins/image-prioritizer/tests/test-cases/multiple-videos-with-desktop-metrics-missing/expected.html index 9dd8a10282..794e0e760d 100644 --- a/plugins/image-prioritizer/tests/test-cases/multiple-videos-with-desktop-metrics-missing/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/multiple-videos-with-desktop-metrics-missing/expected.html @@ -8,13 +8,14 @@
- - - - - + + + + +
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/no-url-metrics-but-server-side-heuristics-added-fetchpriority-high/expected.html b/plugins/image-prioritizer/tests/test-cases/no-url-metrics-but-server-side-heuristics-added-fetchpriority-high/expected.html index dcc0d86109..8d7db55086 100644 --- a/plugins/image-prioritizer/tests/test-cases/no-url-metrics-but-server-side-heuristics-added-fetchpriority-high/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/no-url-metrics-but-server-side-heuristics-added-fetchpriority-high/expected.html @@ -7,12 +7,13 @@
- Foo - Bar - Baz - Qux + Foo + Bar + Baz + Qux
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/no-url-metrics/expected.html b/plugins/image-prioritizer/tests/test-cases/no-url-metrics/expected.html index be90c68224..dd6b860d4c 100644 --- a/plugins/image-prioritizer/tests/test-cases/no-url-metrics/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/no-url-metrics/expected.html @@ -7,11 +7,12 @@
- Foo + Foo

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

-
This is so background!
+
This is so background!
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html index f8e4c6d58e..fab4b3aa78 100644 --- a/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/only-mobile-and-desktop-groups-are-populated/expected.html @@ -26,18 +26,19 @@

Last Post

First Post

This post does have a featured image, and the server-side heuristics in WordPress cause it to get fetchpriority=high, but it should not have this since it is out of the viewport on mobile.

Pretend this is a super long paragraph that pushes the next div out of the initial viewport.

-
This is so background!
+
This is so background!
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html b/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html index 424fcbf30e..403b38c6f6 100644 --- a/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/picture-element-as-lcp-tablet-and-desktop-metrics-missing/expected.html @@ -11,10 +11,11 @@ - Foo + Foo - + + diff --git a/plugins/image-prioritizer/tests/test-cases/preload-links-with-one-half-stale-group/expected.html b/plugins/image-prioritizer/tests/test-cases/preload-links-with-one-half-stale-group/expected.html index 06c23047a4..c12e314e8c 100644 --- a/plugins/image-prioritizer/tests/test-cases/preload-links-with-one-half-stale-group/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/preload-links-with-one-half-stale-group/expected.html @@ -17,7 +17,7 @@
- Foo + Foo
@@ -26,7 +26,7 @@
- Bar + Bar
@@ -35,7 +35,8 @@ - + + diff --git a/plugins/image-prioritizer/tests/test-cases/url-metric-only-captured-for-one-breakpoint/expected.html b/plugins/image-prioritizer/tests/test-cases/url-metric-only-captured-for-one-breakpoint/expected.html index 5149939a4a..c1e679accc 100644 --- a/plugins/image-prioritizer/tests/test-cases/url-metric-only-captured-for-one-breakpoint/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/url-metric-only-captured-for-one-breakpoint/expected.html @@ -7,10 +7,11 @@
- Foo + Foo
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-collected/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-collected/expected.html index 9affd16b82..99c573efcb 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-collected/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-collected/expected.html @@ -7,12 +7,13 @@
-
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-missing/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-missing/expected.html index 475b4ff231..53843a5961 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-missing/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-with-large-poster-and-desktop-url-metrics-missing/expected.html @@ -7,12 +7,13 @@
-
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints/expected.html index e4aaeaa6ca..3c4ed16dc8 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-all-breakpoints/expected.html @@ -8,9 +8,10 @@
- +
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only/expected.html index c3cceac3ea..fa0515f2d7 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-desktop-only/expected.html @@ -9,13 +9,14 @@
- Mobile Logo -
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet/expected.html b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet/expected.html index a6c1367aad..c2c6e99b09 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-with-poster-lcp-element-on-mobile-and-desktop-but-not-tablet/expected.html @@ -10,10 +10,11 @@
- Tablet header - + Tablet header +
- + + diff --git a/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only/expected.html b/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only/expected.html index 56bdb77fe3..83c92eeff4 100644 --- a/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only/expected.html +++ b/plugins/image-prioritizer/tests/test-cases/video-without-poster-lcp-element-on-desktop-only/expected.html @@ -8,13 +8,14 @@
- Mobile Logo -
- + + diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index e87ec50463..f655587fec 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -23,6 +23,7 @@ * @typedef {import("./types.ts").ExtendRootDataFunction} ExtendRootDataFunction * @typedef {import("./types.ts").GetElementDataFunction} GetElementDataFunction * @typedef {import("./types.ts").ExtendElementDataFunction} ExtendElementDataFunction + * @typedef {import("./types.ts").GetElementXPathFunction} GetElementXPathFunction * @typedef {import("./types.ts").Logger} Logger */ @@ -359,6 +360,97 @@ function extendRootData( properties ) { */ const elementsByXPath = new Map(); +/** + * XPaths for tracked elements, keyed by the auto-incremented ID referenced in each element's data-od-id attribute. + * + * @see {getXPathIdMap} + * @type {string[]} + */ +let xpathIdMap = []; + +/** + * Gets the XPath for an element based on its data-od-id attribute. + * + * @type {GetElementXPathFunction} + * @param {Element} element - Element. + * @return {string|null} XPath for the element, or null if the element is not tracked. + */ +function getElementXPath( element ) { + const id = element.getAttribute( 'data-od-id' ); + if ( null === id ) { + return null; + } + const xpath = xpathIdMap[ Number( id ) ]; + return 'string' === typeof xpath ? xpath : null; +} + +/** + * Fetches and decodes the mapping of auto-incremented IDs to XPaths for tracked elements. + * + * The mapping is embedded in a SCRIPT tag added by PHP. To keep the HTML payload small, it is normally + * gzip-compressed and base64-encoded, but it falls back to being plain JSON when gzip is unavailable server-side. + * This avoids putting XPath strings directly into HTML attributes, where they can be corrupted by plugins which use + * naive regular expressions to strip JS/CSS comments from the page (mistaking XPath's /* for the start of one). + * + * @see https://github.com/WordPress/performance/issues/1947 + * + * @param {Pick} logger - Logger. + * @return {Promise} XPaths for tracked elements, keyed by their auto-incremented ID. + */ +async function getXPathIdMap( { warn, error } ) { + const script = doc.querySelector( + 'script#optimization-detective-xpath-map' + ); + if ( ! ( script instanceof HTMLScriptElement ) ) { + return []; + } + + const type = script.type.toLowerCase(); + + /** @type {string} */ + let json; + if ( 'application/json' === type ) { + json = script.text; + } else if ( 'application/gzip+json;base64' === type ) { + if ( typeof DecompressionStream === 'undefined' ) { + error( + 'Unable to decode the XPath ID map since DecompressionStream is not supported in this browser.' + ); + return []; + } + try { + const binaryString = atob( script.text ); + const bytes = Uint8Array.from( binaryString, ( char ) => + char.charCodeAt( 0 ) + ); + const decompressedStream = new Blob( [ bytes ] ) + .stream() + .pipeThrough( new DecompressionStream( 'gzip' ) ); + json = await new Response( decompressedStream ).text(); + } catch ( err ) { + error( 'Failed to decompress the XPath ID map:', err ); + return []; + } + } else { + warn( `Unexpected type for the XPath ID map script: ${ type }` ); + return []; + } + + try { + const parsed = JSON.parse( json ); + if ( + Array.isArray( parsed ) && + parsed.every( ( xpath ) => 'string' === typeof xpath ) + ) { + return parsed; + } + error( 'Parsed XPath ID map is not an array of strings.' ); + } catch ( err ) { + error( 'Failed to parse the XPath ID map JSON:', err ); + } + return []; +} + /** * Reserved element property keys. * @@ -694,23 +786,23 @@ export default async function detect( { log( 'Proceeding with detection' ); - const breadcrumbedElements = doc.body.querySelectorAll( '[data-od-xpath]' ); + xpathIdMap = await getXPathIdMap( { warn, error } ); + + const breadcrumbedElements = doc.body.querySelectorAll( '[data-od-id]' ); /** @type {Map} */ - const breadcrumbedElementsMap = new Map( - [ ...breadcrumbedElements ].map( - /** - * @param {Element} element - * @return {[Element, string]} Tuple of an element and its XPath. - */ - ( element ) => [ - element, - /** @type {string} */ ( - element.getAttribute( 'data-od-xpath' ) - ), - ] - ) - ); + const breadcrumbedElementsMap = new Map(); + for ( const element of breadcrumbedElements ) { + const xpath = getElementXPath( element ); + if ( null === xpath ) { + warn( + 'Unable to resolve XPath for element with data-od-id:', + element + ); + continue; + } + breadcrumbedElementsMap.set( element, xpath ); + } /** @type {IntersectionObserverEntry[]} */ const elementIntersections = []; @@ -883,6 +975,7 @@ export default async function detect( { extendRootData, getElementData, extendElementData, + getElementXPath, } ); if ( initializePromise instanceof Promise ) { extensionInitializePromises.push( initializePromise ); @@ -989,6 +1082,7 @@ export default async function detect( { getElementData, extendElementData, extendRootData, + getElementXPath, } ); if ( finalizePromise instanceof Promise ) { extensionFinalizePromises.push( finalizePromise ); diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index bce299cd69..6da7a53ff5 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -71,12 +71,16 @@ function od_get_cache_purge_post_id(): ?int { * * @since 0.1.0 * @since 1.0.0 Renamed from od_get_detection_script(). + * @since n.e.x.t Added $xpath_id_map parameter to emit the XPath ID mapping script. * @access private * + * @phpstan-param list $xpath_id_map + * * @param non-empty-string $slug URL Metrics slug. * @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection. + * @param string[] $xpath_id_map XPaths for tracked elements, keyed by the auto-incremented ID referenced in each element's data-od-id attribute. */ -function od_get_detection_scripts( string $slug, OD_URL_Metric_Group_Collection $group_collection ): string { +function od_get_detection_scripts( string $slug, OD_URL_Metric_Group_Collection $group_collection, array $xpath_id_map ): string { /** * Filters whether to use the web-vitals.js build with attribution. @@ -189,7 +193,31 @@ function od_get_detection_scripts( string $slug, OD_URL_Metric_Group_Collection array( 'type' => 'module' ) ); - return $json_script . $module_script; + $xpath_map_script = ''; + if ( count( $xpath_id_map ) > 0 ) { + $xpath_map_json = (string) wp_json_encode( $xpath_id_map, $json_flags ); + + if ( function_exists( 'gzencode' ) ) { + $compressed_xpath_map = gzencode( $xpath_map_json ); + $xpath_map_script = wp_get_inline_script_tag( + false !== $compressed_xpath_map ? base64_encode( $compressed_xpath_map ) : $xpath_map_json, // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode -- base64_encode() is used here to safely embed the gzip-compressed binary payload inside a SCRIPT tag, not to obfuscate code. + array( + 'type' => false !== $compressed_xpath_map ? 'application/gzip+json;base64' : 'application/json', + 'id' => 'optimization-detective-xpath-map', + ) + ); + } else { + $xpath_map_script = wp_get_inline_script_tag( + $xpath_map_json, + array( + 'type' => 'application/json', + 'id' => 'optimization-detective-xpath-map', + ) + ); + } + } + + return $json_script . $xpath_map_script . $module_script; } /** diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php index 07132b37b8..a74a61c153 100644 --- a/plugins/optimization-detective/optimization.php +++ b/plugins/optimization-detective/optimization.php @@ -267,9 +267,17 @@ function od_optimize_template_output_buffer( string $buffer ): string { $current_tag_bookmark = 'optimization_detective_current_tag'; $visitors = iterator_to_array( $tag_visitor_registry ); - // Whether we need to add the data-od-xpath attribute to elements and whether the detection script should be injected. + // Whether we need to add the data-od-id attribute to elements and whether the detection script should be injected. $needs_detection = ! $group_collection->is_every_group_complete(); $did_amend_meta_generator = false; + + /** + * XPaths for tracked elements, keyed by the auto-incremented ID referenced in each element's data-od-id attribute. + * + * @var list $xpath_id_map + */ + $xpath_id_map = array(); + do { // Never process anything inside NOSCRIPT since it will never show up in the DOM when scripting is enabled, and thus it can never be detected nor measured. // Similarly, elements in the Admin Bar are not relevant for optimization, so this loop ensures that no tags in the Admin Bar are visited. @@ -318,7 +326,7 @@ function od_optimize_template_output_buffer( string $buffer ): string { } // If the visitor traversed HTML tags, we need to go back to this tag so that in the next iteration any - // relevant tag visitors may apply, in addition to properly setting the data-od-xpath on this tag below. + // relevant tag visitors may apply, in addition to properly setting the data-od-id on this tag below. if ( $cursor_move_count !== $processor->get_cursor_move_count() ) { $processor->seek( $current_tag_bookmark ); // TODO: Should this break out of the optimization loop if it returns false? } @@ -330,16 +338,17 @@ function od_optimize_template_output_buffer( string $buffer ): string { } if ( $tracked_in_url_metrics && $needs_detection ) { - $processor->set_meta_attribute( 'xpath', $processor->get_xpath() ); + $xpath_id_map[] = $processor->get_xpath(); + $processor->set_meta_attribute( 'id', (string) ( count( $xpath_id_map ) - 1 ) ); } $visited_tag_state->reset(); } while ( $processor->next_tag( array( 'tag_closers' => 'skip' ) ) ); // Inject detection script. - // TODO: When optimizing above, if we find that there is a stored LCP element but it fails to match, it should perhaps set $needs_detection to true and send the request with an override nonce. However, this would require backtracking and adding the data-od-xpath attributes. + // TODO: When optimizing above, if we find that there is a stored LCP element but it fails to match, it should perhaps set $needs_detection to true and send the request with an override nonce. However, this would require backtracking and adding the data-od-id attributes. if ( $needs_detection ) { - $processor->append_body_html( od_get_detection_scripts( $slug, $group_collection ) ); + $processor->append_body_html( od_get_detection_scripts( $slug, $group_collection, $xpath_id_map ) ); } /** diff --git a/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php b/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php index 65641b0be0..cfbc7da42e 100644 --- a/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php +++ b/plugins/optimization-detective/tests/class-optimization-detective-test-helpers.php @@ -332,6 +332,20 @@ public function assert_snapshot_equals( string $directory ): void { } $buffer = $processor->get_updated_html(); + // Normalize XPath ID map script content so the gzip/base64-encoded (or plain JSON) payload does not impact snapshots. + $processor = new WP_HTML_Tag_Processor( $buffer ); + while ( $processor->next_tag( array( 'tag_name' => 'SCRIPT' ) ) ) { + if ( 'optimization-detective-xpath-map' !== $processor->get_attribute( 'id' ) ) { + continue; + } + + $text = $processor->get_modifiable_text(); + if ( '' !== $text ) { + $processor->set_modifiable_text( '__XPATH_ID_MAP__' ); + } + } + $buffer = $processor->get_updated_html(); + // Normalize style content so changes do not impact snapshots. $processor = new WP_HTML_Tag_Processor( $buffer ); while ( $processor->next_tag( array( 'tag_name' => 'STYLE' ) ) ) { diff --git a/plugins/optimization-detective/tests/test-cases/admin-bar/expected.html b/plugins/optimization-detective/tests/test-cases/admin-bar/expected.html index 24f10e3cf2..47b9b44a2c 100644 --- a/plugins/optimization-detective/tests/test-cases/admin-bar/expected.html +++ b/plugins/optimization-detective/tests/test-cases/admin-bar/expected.html @@ -13,10 +13,11 @@

- +

- + + diff --git a/plugins/optimization-detective/tests/test-cases/many-images/expected.html b/plugins/optimization-detective/tests/test-cases/many-images/expected.html index e131a66191..e1745937c9 100644 --- a/plugins/optimization-detective/tests/test-cases/many-images/expected.html +++ b/plugins/optimization-detective/tests/test-cases/many-images/expected.html @@ -7,1008 +7,1009 @@
- Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo - Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo + Foo
- + + diff --git a/plugins/optimization-detective/tests/test-cases/no-url-metrics/expected.html b/plugins/optimization-detective/tests/test-cases/no-url-metrics/expected.html index 97952c4ef1..f276c33626 100644 --- a/plugins/optimization-detective/tests/test-cases/no-url-metrics/expected.html +++ b/plugins/optimization-detective/tests/test-cases/no-url-metrics/expected.html @@ -7,9 +7,10 @@
- Foo + Foo
- + + diff --git a/plugins/optimization-detective/tests/test-cases/preload-link/expected.html b/plugins/optimization-detective/tests/test-cases/preload-link/expected.html index 16780c57e7..1869b80368 100644 --- a/plugins/optimization-detective/tests/test-cases/preload-link/expected.html +++ b/plugins/optimization-detective/tests/test-cases/preload-link/expected.html @@ -8,9 +8,10 @@
- +
- + + diff --git a/plugins/optimization-detective/tests/test-cases/stray-closing-p/expected.html b/plugins/optimization-detective/tests/test-cases/stray-closing-p/expected.html index b2f01f5966..8b0837df0a 100644 --- a/plugins/optimization-detective/tests/test-cases/stray-closing-p/expected.html +++ b/plugins/optimization-detective/tests/test-cases/stray-closing-p/expected.html @@ -8,11 +8,12 @@
-

A bison
This is a bison.

-
Another bison
This is another bison.

- Hero +

A bison
This is a bison.

+
Another bison
This is another bison.

+ Hero
+ diff --git a/plugins/optimization-detective/tests/test-cases/tag-track-opt-in/expected.html b/plugins/optimization-detective/tests/test-cases/tag-track-opt-in/expected.html index f45dfcfd34..1a338ee9ae 100644 --- a/plugins/optimization-detective/tests/test-cases/tag-track-opt-in/expected.html +++ b/plugins/optimization-detective/tests/test-cases/tag-track-opt-in/expected.html @@ -8,11 +8,12 @@
-
-
-
+
+
+
- + + diff --git a/plugins/optimization-detective/tests/test-cases/video/expected.html b/plugins/optimization-detective/tests/test-cases/video/expected.html index fe2985b156..55c450b44f 100644 --- a/plugins/optimization-detective/tests/test-cases/video/expected.html +++ b/plugins/optimization-detective/tests/test-cases/video/expected.html @@ -7,12 +7,13 @@
-
- + + diff --git a/plugins/optimization-detective/tests/test-cases/xhtml-response/expected.html b/plugins/optimization-detective/tests/test-cases/xhtml-response/expected.html index c564bcab6a..66e568a1b2 100644 --- a/plugins/optimization-detective/tests/test-cases/xhtml-response/expected.html +++ b/plugins/optimization-detective/tests/test-cases/xhtml-response/expected.html @@ -9,9 +9,10 @@
-

Foo

+

Foo

- + + diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index 10df8b6ea2..5b0b380103 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -219,7 +219,7 @@ public function test_od_get_detection_scripts_returns_script( Closure $set_up, a $breakpoints = array( 480, 600, 782 ); $group_collection = new OD_URL_Metric_Group_Collection( array(), $current_etag, $breakpoints, 3, HOUR_IN_SECONDS ); - $script = od_get_detection_scripts( $slug, $group_collection ); + $script = od_get_detection_scripts( $slug, $group_collection, array() ); $this->assertStringContainsString( '#s', + $script, + $matches + ) + ); + list( , $type, $contents ) = $matches; + $contents = trim( $contents ); + + if ( function_exists( 'gzencode' ) ) { + $this->assertSame( 'application/gzip+json;base64', $type ); + $decoded = base64_decode( $contents, true ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode -- base64_decode() is used here to verify the gzip-compressed payload, not to obfuscate code. + $this->assertIsString( $decoded ); + $json = gzdecode( $decoded ); + $this->assertIsString( $json ); + } else { + $this->assertSame( 'application/json', $type ); + $json = $contents; + } + + $this->assertSame( $xpath_id_map, json_decode( $json, true ) ); + } + /** * Test od_register_rest_url_metric_store_endpoint(). * diff --git a/plugins/optimization-detective/types.ts b/plugins/optimization-detective/types.ts index 39533aed39..38c8e65fa0 100644 --- a/plugins/optimization-detective/types.ts +++ b/plugins/optimization-detective/types.ts @@ -65,6 +65,7 @@ export type ExtendElementDataFunction = ( xpath: string, properties: ExtendedElementData ) => void; +export type GetElementXPathFunction = ( element: Element ) => string | null; export type InitializeArgs = { readonly isDebug: boolean; @@ -81,6 +82,7 @@ export type InitializeArgs = { readonly extendRootData: ExtendRootDataFunction; readonly getElementData: GetElementDataFunction; readonly extendElementData: ExtendElementDataFunction; + readonly getElementXPath: GetElementXPathFunction; }; export type InitializeCallback = ( args: InitializeArgs ) => Promise< void >; @@ -90,6 +92,7 @@ export type FinalizeArgs = { readonly extendRootData: ExtendRootDataFunction; readonly getElementData: GetElementDataFunction; readonly extendElementData: ExtendElementDataFunction; + readonly getElementXPath: GetElementXPathFunction; readonly isDebug: boolean; readonly log: LogFunction; readonly info: LogFunction;