Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow extensions to opt-in to using the web-vitals attribution build via the od_use_web_vitals_attribution_build filter #1759

Merged
merged 45 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
fc95f4a
Bundle web-vitals attribution build
swissspidy Dec 18, 2024
651577e
Allow loading web-vitals attribution build
swissspidy Dec 18, 2024
ff3f120
Collect some INP data for testing
swissspidy Dec 18, 2024
9dd8159
Add debugging helper for LCP elements
swissspidy Dec 18, 2024
12a3494
some lint fixes
swissspidy Dec 18, 2024
be0be49
Add INP elements with JS
swissspidy Dec 18, 2024
e9bbe99
Apply suggestions from code review
swissspidy Dec 19, 2024
ac4d153
Add missing return statement
swissspidy Dec 19, 2024
098191c
Lint fixes
swissspidy Dec 19, 2024
9288025
Clarify comment
swissspidy Dec 19, 2024
2f507a0
PHPStan fixes
swissspidy Dec 19, 2024
8ead9ae
Multiline comment
swissspidy Dec 19, 2024
ad09dea
Merge branch 'trunk' into try/1736-debug-detective
swissspidy Dec 19, 2024
3e4d648
Add empty line
swissspidy Dec 19, 2024
c8b490b
Merge branch 'trunk' into try/1736-debug-detective
swissspidy Jan 10, 2025
36430ad
Only load assets if admin bar is showing
swissspidy Jan 10, 2025
a229ba6
Extract `od_get_group_collection()` helper
swissspidy Jan 10, 2025
ba48bd9
Don't add visitor if not showing admin bar
swissspidy Jan 10, 2025
581ffbc
Add `additionalProperties`
swissspidy Jan 10, 2025
eabd8c8
Fix anchors and popovers
swissspidy Jan 10, 2025
ebfea3f
Re-enable css
swissspidy Jan 10, 2025
c2d1a9f
Fix test_get_json_schema
westonruter Jan 13, 2025
6d95ce1
Prevent warning if INP data is missing
swissspidy Jan 13, 2025
5de84c6
Remove admin bar condition in admin bar cb
swissspidy Jan 13, 2025
adf3ae5
Styling adjustments
swissspidy Jan 13, 2025
e7432ed
Remove dev mode condition
swissspidy Jan 13, 2025
d552366
Move INP logic to tag visitor too
swissspidy Jan 13, 2025
2aaad52
Undo `od_get_group_collection`
swissspidy Jan 13, 2025
39f8336
Lint fixes
swissspidy Jan 13, 2025
1d900d2
Reuse existing anchor name
swissspidy Jan 13, 2025
efc1aa6
phpstan fixes
swissspidy Jan 13, 2025
b63b9d2
lint fix
swissspidy Jan 13, 2025
6d848d5
Merge branch 'trunk' into try/1736-debug-detective
swissspidy Jan 13, 2025
2f162f2
Remove extracted code
swissspidy Jan 13, 2025
89b718a
Merge branch 'trunk' into try/1736-debug-detective
swissspidy Jan 13, 2025
d32ffc0
Undo tests change too
swissspidy Jan 13, 2025
2802b57
Remove JS parts now as well
swissspidy Jan 14, 2025
5fd6891
Undo type import
swissspidy Jan 14, 2025
8dd8af6
Don't conditionally load by default
swissspidy Jan 14, 2025
c6f53fa
Remove newline
swissspidy Jan 14, 2025
fd7fa9f
Add jsdoc description
westonruter Jan 14, 2025
3614c13
Add typing for attribution report functions
westonruter Jan 14, 2025
3a30027
Add docs for the od_use_web_vitals_attribution_build filter
westonruter Jan 14, 2025
a75dd53
Add test for od_use_web_vitals_attribution_build filter
westonruter Jan 14, 2025
9600078
fixup! Add docs for the od_use_web_vitals_attribution_build filter
westonruter Jan 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
<?php
/**
* Image Prioritizer: IP_Img_Tag_Visitor class
*
* @package image-prioritizer
* @since n.e.x.t
*/

// Exit if accessed directly.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}

/**
* Tag visitor that optimizes IMG tags.
*
* @phpstan-import-type LinkAttributes from OD_Link_Collection
*
* @since n.e.x.t
* @access private
*/
final class Optimization_Detective_Debug_Tag_Visitor {

/**
* Visits a tag.
*
* @since n.e.x.t
*
* @param OD_Tag_Visitor_Context $context Tag visitor context.
* @return bool Whether the tag should be tracked in URL Metrics.
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
*/
public function __invoke( OD_Tag_Visitor_Context $context ): bool {
$processor = $context->processor;

if ( ! $context->url_metric_group_collection->is_any_group_populated() ) {
return false;
}

$xpath = $processor->get_xpath();

$visited = false;

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
/**
* @var OD_URL_Metric_Group $group
*/
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',
$group->get_minimum_viewport_width()
);

$processor->set_attribute(
'style',
"--anchor-name: --od-debug-element-$uuid;" . $processor->get_attribute( '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(
<<<HTML
<button
class="od-debug-dot"
type="button"
popovertarget="od-debug-popover-$uuid"
popovertargetaction="toggle"
style="--anchor-name: --od-debug-dot-$uuid; position-anchor: --od-debug-element-$uuid;"
aria-details="od-debug-popover-$uuid"
>
$anchor_text
</button>
<div
id="od-debug-popover-$uuid"
popover
class="od-debug-popover"
style="position-anchor: --od-debug-dot-$uuid;"
>
$popover_text
</div>
HTML
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
);

$visited = true;
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
}
}

return $visited;
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
}
}
217 changes: 217 additions & 0 deletions plugins/optimization-detective/debug.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
<?php
/**
* Debug helpers used for Optimization Detective.
*
* @package optimization-detective
* @since n.e.x.t
*/

if ( ! defined( 'ABSPATH' ) ) {
exit; // Exit if accessed directly.
}

/**
* Registers tag visitors.
*
* @since n.e.x.t
*
* @param OD_Tag_Visitor_Registry $registry Tag visitor registry.
*/
function od_debug_register_tag_visitors( OD_Tag_Visitor_Registry $registry ): void {
$debug_visitor = new Optimization_Detective_Debug_Tag_Visitor();
$registry->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<string, array{type: string}> $additional_properties Additional properties.
* @return array<string, array{type: string}> 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.
'required' => false,
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
'items' => array(
'type' => 'object',
'required' => true,
'properties' => array(
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
'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");',
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
),
)
);
}

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;
}
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

$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()
);
swissspidy marked this conversation as resolved.
Show resolved Hide resolved

$inp_dots = array();

/**
* @var OD_URL_Metric_Group $group
*/
foreach ( $group_collection as $group ) {
/**
* @var OD_URL_Metric $url_metric
*/
foreach ( $group as $url_metric ) {
foreach ( $url_metric->get( 'inpData' ) as $inp_data ) {
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
if ( isset( $inp_dots[ $inp_data['interactionTarget'] ] ) ) {
$inp_dots[ $inp_data['interactionTarget'] ][] = $inp_data;
} else {
$inp_dots[ $inp_data['interactionTarget'] ] = array( $inp_data );
}
}
}
}

?>
<script>
/* TODO: Add INP elements here */
let count = 0;
for ( const [ interactionTarget, entries ] of Object.entries( <?php echo wp_json_encode( $inp_dots ); ?> ) ) {
const el = document.querySelector( interactionTarget );
if ( ! el ) {
continue;
}

count++;

const anchor = document.createElement( 'button' );
anchor.setAttribute( 'class', 'od-debug-dot' );
anchor.setAttribute( 'popovertarget', `od-debug-popover-${count}` );
anchor.setAttribute( 'popovertargetaction', 'toggle' );
anchor.setAttribute( 'style', `--anchor-name: --od-debug-dot-${count}; position-anchor: --od-debug-element-${count};` );
anchor.setAttribute( 'aria-details', `od-debug-popover-${count}` );
anchor.textContent = 'Optimization Detective';

const tooltip = document.createElement( 'div' );
tooltip.setAttribute( 'id', `od-debug-popover-${count}` );
tooltip.setAttribute( 'popover', '' );
tooltip.setAttribute( 'class', 'od-debug-popover' );
tooltip.setAttribute( 'style', `position-anchor: --od-debug-dot-${count};` );
tooltip.textContent = `INP Element (Value: ${entries[0].value}) (Rating: ${entries[0].rating})`;

document.body.append(anchor);
document.body.append(tooltip);
}
</script>
<style>
body:not(.od-debug) .od-debug-dot,
body:not(.od-debug) .od-debug-popover {
/*display: none;*/
}

.od-debug-dot {
height: 2em;
width: 2em;
background: rebeccapurple;
border-radius: 50%;
animation: pulse 2s infinite;
position: absolute;
position-area: center center;
margin: 5px 0 0 5px;
}

.od-debug-popover {
position: absolute;
position-area: right;
margin: 5px 0 0 5px;
}

@keyframes pulse {
0% {
transform: scale(0.8);
opacity: 0.5;
box-shadow: 0 0 0 0 rgba(102, 51, 153, 0.7);
}
70% {
transform: scale(1);
opacity: 1;
box-shadow: 0 0 0 10px rgba(102, 51, 153, 0);
}
100% {
transform: scale(0.8);
opacity: 0.5;
box-shadow: 0 0 0 0 rgba(102, 51, 153, 0);
}
}
</style>
<?php
}

add_action( 'wp_footer', 'od_debug_add_assets' );
34 changes: 32 additions & 2 deletions plugins/optimization-detective/detect.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
/**
* @typedef {import("web-vitals").LCPMetric} LCPMetric
* @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution
* @typedef {import("web-vitals").INPMetric} INPMetric
* @typedef {import("web-vitals").INPMetricWithAttribution} INPMetricWithAttribution
* @typedef {import("./types.ts").ElementData} ElementData
* @typedef {import("./types.ts").OnTTFBFunction} OnTTFBFunction
* @typedef {import("./types.ts").OnFCPFunction} OnFCPFunction
Expand Down Expand Up @@ -490,13 +493,17 @@ export default async function detect( {
} );
}

/** @type {LCPMetric[]} */
/** @type {(LCPMetric|LCPMetricWithAttribution)[]} */
const lcpMetricCandidates = [];

// Obtain at least one LCP candidate. More may be reported before the page finishes loading.
await new Promise( ( resolve ) => {
onLCP(
( /** @type LCPMetric */ metric ) => {
/**
*
* @param {LCPMetric|LCPMetricWithAttribution} metric
*/
( metric ) => {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this go back to being defined inline? The type is picked up for me in PhpStorm:

image

Suggested change
/**
*
* @param {LCPMetric|LCPMetricWithAttribution} metric
*/
( metric ) => {
( /** @type {LCPMetric|LCPMetricWithAttribution} */ metric ) => {

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

But your PhpStorm clearly only sees LCPMetric, not LCPMetricWithAttribution

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point!

What's strange to me is why this typing is even needed.

With 3614c13 I added more typing for the attribution report functions. Then PHPStorm reports that the arg is detected as a LCPMetricWithAttribution:

image

But TypeScript is giving a warning about it being implicit any:

image

Nevertheless, onLCP is defined as being either OnLCPFunction or OnLCPWithAttributionFunction:

image

So I would have thought that this would be later be reflected in the type of the arg here. But instead it is showing up as any, even though the common ReportOpts arg is carried through:

image

lcpMetricCandidates.push( metric );
resolve();
},
Expand All @@ -511,6 +518,26 @@ export default async function detect( {

// Stop observing.
disconnectIntersectionObserver();

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
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,
} );
}
}
);

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
if ( isDebug ) {
log( 'Detection is stopping.' );
}
Expand All @@ -522,6 +549,7 @@ export default async function detect( {
height: win.innerHeight,
},
elements: [],
inpData: [],
swissspidy marked this conversation as resolved.
Show resolved Hide resolved
};

const lcpMetric = lcpMetricCandidates.at( -1 );
Expand Down Expand Up @@ -581,6 +609,8 @@ export default async function detect( {
);
} );

urlMetric.inpData = inpData;

swissspidy marked this conversation as resolved.
Show resolved Hide resolved
// 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 ) {
Expand Down
Loading
Loading