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`,