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..d47a60b3e
--- /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(
+ <<
+ $anchor_text
+
+
+ $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..06c059577
--- /dev/null
+++ b/plugins/optimization-detective/debug.php
@@ -0,0 +1,214 @@
+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,
+ ),
+ ),
+ ),
+ );
+ 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() ) {
+ 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 ( ! od_can_optimize_response() ) {
+ return;
+ }
+
+ $slug = od_get_url_metrics_slug( od_get_normalized_query_vars() );
+ $post = OD_URL_Metrics_Post_Type::get_post( $slug );
+
+ global $wp_the_query;
+
+ $tag_visitor_registry = new OD_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()
+ );
+
+ $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 2fa2a6dee..13ba6bcda 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 = add_query_arg( 'ver', $web_vitals_lib_data['version'], plugin_dir_url( __FILE__ ) . 'build/web-vitals.js' );
+ $web_vitals_lib_src = $use_attribution_build ?
+ plugin_dir_url( __FILE__ ) . 'build/web-vitals-attribution.js' :
+ plugin_dir_url( __FILE__ ) . 'build/web-vitals.js';
+ $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/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/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`,