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