diff --git a/plugins/optimization-detective/class-optimization-detective-debug-tag-visitor.php b/plugins/optimization-detective/class-optimization-detective-debug-tag-visitor.php
new file mode 100644
index 000000000..65c66a7f3
--- /dev/null
+++ b/plugins/optimization-detective/class-optimization-detective-debug-tag-visitor.php
@@ -0,0 +1,95 @@
+processor;
+
+ if ( ! $context->url_metric_group_collection->is_any_group_populated() ) {
+ return false;
+ }
+
+ $xpath = $processor->get_xpath();
+
+ foreach ( $context->url_metric_group_collection as $group ) {
+ // This is the LCP element for this group.
+ if ( $group->get_lcp_element() instanceof OD_Element && $xpath === $group->get_lcp_element()->get_xpath() ) {
+ $uuid = wp_generate_uuid4();
+
+ $processor->set_meta_attribute(
+ 'viewport',
+ (string) $group->get_minimum_viewport_width()
+ );
+
+ $style = $processor->get_attribute( 'style' );
+ $style = is_string( $style ) ? $style : '';
+ $processor->set_attribute(
+ 'style',
+ "anchor-name: --od-debug-element-$uuid;" . $style
+ );
+
+ $processor->set_meta_attribute(
+ 'debug-is-lcp',
+ true
+ );
+
+ $anchor_text = __( 'Optimization Detective', 'optimization-detective' );
+ $popover_text = __( 'LCP Element', 'optimization-detective' );
+
+ $processor->append_body_html(
+ <<
+
+
+ $popover_text
+
+HTML
+ );
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/plugins/optimization-detective/debug.php b/plugins/optimization-detective/debug.php
new file mode 100644
index 000000000..9d7a23334
--- /dev/null
+++ b/plugins/optimization-detective/debug.php
@@ -0,0 +1,226 @@
+register( 'optimization-detective/debug', $debug_visitor );
+}
+
+add_action( 'od_register_tag_visitors', 'od_debug_register_tag_visitors', PHP_INT_MAX );
+
+/**
+ * Filters additional properties for the element item schema for Optimization Detective.
+ *
+ * @since n.e.x.t
+ *
+ * @param array $additional_properties Additional properties.
+ * @return array Additional properties.
+ */
+function od_debug_add_inp_schema_properties( array $additional_properties ): array {
+ $additional_properties['inpData'] = array(
+ 'description' => __( 'INP metrics', 'optimization-detective' ),
+ 'type' => 'array',
+
+ /*
+ * All extended properties must be optional so that URL Metrics are not all immediately invalidated once an extension is deactivated.
+ * Also, no INP data will be sent if the user never interacted with the page.
+ */
+ 'required' => false,
+ 'items' => array(
+ 'type' => 'object',
+ 'required' => true,
+ 'properties' => array(
+ 'value' => array(
+ 'type' => 'number',
+ 'required' => true,
+ ),
+ 'rating' => array(
+ 'type' => 'string',
+ 'enum' => array( 'good', 'needs-improvement', 'poor' ),
+ 'required' => true,
+ ),
+ 'interactionTarget' => array(
+ 'type' => 'string',
+ 'required' => true,
+ ),
+ ),
+ 'additionalProperties' => true,
+ ),
+ );
+ return $additional_properties;
+}
+
+add_filter( 'od_url_metric_schema_root_additional_properties', 'od_debug_add_inp_schema_properties' );
+
+/**
+ * Adds a new admin bar menu item for Optimization Detective debug mode.
+ *
+ * @since n.e.x.t
+ *
+ * @param WP_Admin_Bar $wp_admin_bar The WP_Admin_Bar instance, passed by reference.
+ */
+function od_debug_add_admin_bar_menu_item( WP_Admin_Bar &$wp_admin_bar ): void {
+ if ( ! current_user_can( 'customize' ) && ! wp_is_development_mode( 'plugin' ) ) {
+ return;
+ }
+
+ if ( ! is_admin_bar_showing() ) {
+ return;
+ }
+
+ if ( is_admin() ) {
+ return;
+ }
+
+ $wp_admin_bar->add_menu(
+ array(
+ 'id' => 'optimization-detective-debug',
+ 'parent' => null,
+ 'group' => null,
+ 'title' => __( 'Optimization Detective', 'optimization-detective' ),
+ 'meta' => array(
+ 'onclick' => 'document.body.classList.toggle("od-debug");',
+ ),
+ )
+ );
+}
+
+add_action( 'admin_bar_menu', 'od_debug_add_admin_bar_menu_item', 100 );
+
+/**
+ * Adds inline JS & CSS for debugging.
+ */
+function od_debug_add_assets(): void {
+ if ( ! current_user_can( 'customize' ) && ! wp_is_development_mode( 'plugin' ) ) {
+ return;
+ }
+
+ if ( ! is_admin_bar_showing() ) {
+ return;
+ }
+
+ $tag_visitor_registry = new OD_Tag_Visitor_Registry();
+
+ /** This action is documented in optimization.php. */
+ do_action( 'od_register_tag_visitors', $tag_visitor_registry );
+
+ $group_collection = od_get_group_collection( $tag_visitor_registry );
+
+ $inp_dots = array();
+
+ foreach ( $group_collection as $group ) {
+ foreach ( $group as $url_metric ) {
+ foreach ( $url_metric->get( 'inpData' ) as $inp_data ) {
+ if ( isset( $inp_dots[ $inp_data['interactionTarget'] ] ) ) {
+ $inp_dots[ $inp_data['interactionTarget'] ][] = $inp_data;
+ } else {
+ $inp_dots[ $inp_data['interactionTarget'] ] = array( $inp_data );
+ }
+ }
+ }
+ }
+
+ ?>
+
+
+ {
onLCP(
- ( /** @type LCPMetric */ metric ) => {
+ /**
+ *
+ * @param {LCPMetric|LCPMetricWithAttribution} metric
+ */
+ ( metric ) => {
lcpMetricCandidates.push( metric );
resolve();
},
@@ -511,6 +518,26 @@ export default async function detect( {
// Stop observing.
disconnectIntersectionObserver();
+
+ const inpData = [];
+
+ onINP(
+ /**
+ *
+ * @param {INPMetric|INPMetricWithAttribution} metric
+ */
+ ( metric ) => {
+ if ( 'attribution' in metric ) {
+ // TODO: Store xpath instead?
+ inpData.push( {
+ value: metric.value,
+ rating: metric.rating,
+ interactionTarget: metric.attribution.interactionTarget,
+ } );
+ }
+ }
+ );
+
if ( isDebug ) {
log( 'Detection is stopping.' );
}
@@ -522,6 +549,7 @@ export default async function detect( {
height: win.innerHeight,
},
elements: [],
+ inpData: [],
};
const lcpMetric = lcpMetricCandidates.at( -1 );
@@ -581,6 +609,8 @@ export default async function detect( {
);
} );
+ urlMetric.inpData = inpData;
+
// Only proceed with submitting the URL Metric if viewport stayed the same size. Changing the viewport size (e.g. due
// to resizing a window or changing the orientation of a device) will result in unexpected metrics being collected.
if ( didWindowResize ) {
diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php
index 116249704..b92665ade 100644
--- a/plugins/optimization-detective/detection.php
+++ b/plugins/optimization-detective/detection.php
@@ -70,8 +70,22 @@ function od_get_cache_purge_post_id(): ?int {
* @param OD_URL_Metric_Group_Collection $group_collection URL Metric group collection.
*/
function od_get_detection_script( string $slug, OD_URL_Metric_Group_Collection $group_collection ): string {
+ $use_attribution_build = WP_DEBUG || wp_is_development_mode( 'plugin' );
+
+ /**
+ * Filters whether to use the web-vitals.js build with attribution.
+ *
+ * @since n.e.x.t
+ *
+ * @param bool $use_attribution_build Whether to use the attribution build.
+ */
+ $use_attribution_build = (bool) apply_filters( 'od_use_web_vitals_attribution_build', $use_attribution_build );
+
$web_vitals_lib_data = require __DIR__ . '/build/web-vitals.asset.php';
- $web_vitals_lib_src = plugins_url( add_query_arg( 'ver', $web_vitals_lib_data['version'], 'build/web-vitals.js' ), __FILE__ );
+ $web_vitals_lib_src = $use_attribution_build ?
+ plugins_url( 'build/web-vitals-attribution.js', __FILE__ ) :
+ plugins_url( 'build/web-vitals.js', __FILE__ );
+ $web_vitals_lib_src = add_query_arg( 'ver', $web_vitals_lib_data['version'], $web_vitals_lib_src );
/**
* Filters the list of extension script module URLs to import when performing detection.
diff --git a/plugins/optimization-detective/helper.php b/plugins/optimization-detective/helper.php
index 27073205d..8851107db 100644
--- a/plugins/optimization-detective/helper.php
+++ b/plugins/optimization-detective/helper.php
@@ -107,3 +107,31 @@ function od_get_asset_path( string $src_path, ?string $min_path = null ): string
return $min_path;
}
+
+/**
+ * Get the group collection for the current request.
+ *
+ * @since n.e.x.t
+ * @access private
+ *
+ * @global WP_Query $wp_the_query WP_Query object.
+ *
+ * @param OD_Tag_Visitor_Registry $tag_visitor_registry Tag visitor registry.
+ * @return OD_URL_Metric_Group_Collection Group collection instance.
+ */
+function od_get_group_collection( OD_Tag_Visitor_Registry $tag_visitor_registry ): OD_URL_Metric_Group_Collection {
+ global $wp_the_query;
+
+ $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
+ $post = OD_URL_Metrics_Post_Type::get_post( $slug );
+
+ $current_etag = od_get_current_url_metrics_etag( $tag_visitor_registry, $wp_the_query, od_get_current_theme_template() );
+
+ return new OD_URL_Metric_Group_Collection(
+ $post instanceof WP_Post ? OD_URL_Metrics_Post_Type::get_url_metrics_from_post( $post ) : array(),
+ $current_etag,
+ od_get_breakpoint_max_widths(),
+ od_get_url_metrics_breakpoint_sample_size(),
+ od_get_url_metric_freshness_ttl()
+ );
+}
diff --git a/plugins/optimization-detective/load.php b/plugins/optimization-detective/load.php
index 81b60cb75..adc4d339a 100644
--- a/plugins/optimization-detective/load.php
+++ b/plugins/optimization-detective/load.php
@@ -127,5 +127,9 @@ class_alias( OD_URL_Metric_Group_Collection::class, 'OD_URL_Metrics_Group_Collec
// Add hooks for the above requires.
require_once __DIR__ . '/hooks.php';
+
+ // Debugging helper.
+ require_once __DIR__ . '/class-optimization-detective-debug-tag-visitor.php';
+ require_once __DIR__ . '/debug.php';
}
);
diff --git a/plugins/optimization-detective/optimization.php b/plugins/optimization-detective/optimization.php
index 40b5002a5..b0f7e0933 100644
--- a/plugins/optimization-detective/optimization.php
+++ b/plugins/optimization-detective/optimization.php
@@ -170,14 +170,10 @@ function od_is_response_html_content_type(): bool {
* @since 0.1.0
* @access private
*
- * @global WP_Query $wp_the_query WP_Query object.
- *
* @param string $buffer Template output buffer.
* @return string Filtered template output buffer.
*/
function od_optimize_template_output_buffer( string $buffer ): string {
- global $wp_the_query;
-
// If the content-type is not HTML or the output does not start with '<', then abort since the buffer is definitely not HTML.
if (
! od_is_response_html_content_type() ||
@@ -197,7 +193,6 @@ function od_optimize_template_output_buffer( string $buffer ): string {
}
$slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
- $post = OD_URL_Metrics_Post_Type::get_post( $slug );
$tag_visitor_registry = new OD_Tag_Visitor_Registry();
@@ -210,14 +205,7 @@ function od_optimize_template_output_buffer( string $buffer ): string {
*/
do_action( 'od_register_tag_visitors', $tag_visitor_registry );
- $current_etag = od_get_current_url_metrics_etag( $tag_visitor_registry, $wp_the_query, od_get_current_theme_template() );
- $group_collection = new OD_URL_Metric_Group_Collection(
- $post instanceof WP_Post ? OD_URL_Metrics_Post_Type::get_url_metrics_from_post( $post ) : array(),
- $current_etag,
- od_get_breakpoint_max_widths(),
- od_get_url_metrics_breakpoint_sample_size(),
- od_get_url_metric_freshness_ttl()
- );
+ $group_collection = od_get_group_collection( $tag_visitor_registry );
$link_collection = new OD_Link_Collection();
$tag_visitor_context = new OD_Tag_Visitor_Context( $processor, $group_collection, $link_collection );
$current_tag_bookmark = 'optimization_detective_current_tag';
diff --git a/plugins/optimization-detective/types.ts b/plugins/optimization-detective/types.ts
index d92c53214..d87d443ca 100644
--- a/plugins/optimization-detective/types.ts
+++ b/plugins/optimization-detective/types.ts
@@ -14,6 +14,12 @@ export interface ElementData {
export type ExtendedElementData = ExcludeProps< ElementData >;
+export interface INPData {
+ value: number;
+ rating: string;
+ interactionTarget: string;
+}
+
export interface URLMetric {
url: string;
viewport: {
@@ -21,6 +27,7 @@ export interface URLMetric {
height: number;
};
elements: ElementData[];
+ inpData: INPData[];
}
export type ExtendedRootData = ExcludeProps< URLMetric >;
diff --git a/webpack.config.js b/webpack.config.js
index faaaa3267..21f6a4366 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -190,6 +190,11 @@ const optimizationDetective = ( env ) => {
to: `${ destination }/build/web-vitals.js`,
info: { minimized: true },
},
+ {
+ from: `${ source }/dist/web-vitals.attribution.js`,
+ to: `${ destination }/build/web-vitals-attribution.js`,
+ info: { minimized: true },
+ },
{
from: `${ source }/package.json`,
to: `${ destination }/build/web-vitals.asset.php`,