diff --git a/plugins/optimization-detective/detect.js b/plugins/optimization-detective/detect.js index ded7ff898b..cabcbe6c61 100644 --- a/plugins/optimization-detective/detect.js +++ b/plugins/optimization-detective/detect.js @@ -1,11 +1,17 @@ /** * @typedef {import("web-vitals").LCPMetric} LCPMetric + * @typedef {import("web-vitals").LCPMetricWithAttribution} LCPMetricWithAttribution * @typedef {import("./types.ts").ElementData} ElementData * @typedef {import("./types.ts").OnTTFBFunction} OnTTFBFunction * @typedef {import("./types.ts").OnFCPFunction} OnFCPFunction * @typedef {import("./types.ts").OnLCPFunction} OnLCPFunction * @typedef {import("./types.ts").OnINPFunction} OnINPFunction * @typedef {import("./types.ts").OnCLSFunction} OnCLSFunction + * @typedef {import("./types.ts").OnTTFBWithAttributionFunction} OnTTFBWithAttributionFunction + * @typedef {import("./types.ts").OnFCPWithAttributionFunction} OnFCPWithAttributionFunction + * @typedef {import("./types.ts").OnLCPWithAttributionFunction} OnLCPWithAttributionFunction + * @typedef {import("./types.ts").OnINPWithAttributionFunction} OnINPWithAttributionFunction + * @typedef {import("./types.ts").OnCLSWithAttributionFunction} OnCLSWithAttributionFunction * @typedef {import("./types.ts").URLMetric} URLMetric * @typedef {import("./types.ts").URLMetricGroupStatus} URLMetricGroupStatus * @typedef {import("./types.ts").Extension} Extension @@ -360,11 +366,11 @@ export default async function detect( { ); const { - /** @type OnTTFBFunction */ onTTFB, - /** @type OnFCPFunction */ onFCP, - /** @type OnLCPFunction */ onLCP, - /** @type OnINPFunction */ onINP, - /** @type OnCLSFunction */ onCLS, + /** @type {OnTTFBFunction|OnTTFBWithAttributionFunction} */ onTTFB, + /** @type {OnFCPFunction|OnFCPWithAttributionFunction} */ onFCP, + /** @type {OnLCPFunction|OnLCPWithAttributionFunction} */ onLCP, + /** @type {OnINPFunction|OnINPWithAttributionFunction} */ onINP, + /** @type {OnCLSFunction|OnCLSWithAttributionFunction} */ onCLS, } = await import( webVitalsLibrarySrc ); // TODO: Does this make sense here? @@ -490,13 +496,18 @@ 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 ) => { + /** + * Handles an LCP metric being reported. + * + * @param {LCPMetric|LCPMetricWithAttribution} metric + */ + ( metric ) => { lcpMetricCandidates.push( metric ); resolve(); }, diff --git a/plugins/optimization-detective/detection.php b/plugins/optimization-detective/detection.php index 116249704d..b7d4402f2e 100644 --- a/plugins/optimization-detective/detection.php +++ b/plugins/optimization-detective/detection.php @@ -70,8 +70,32 @@ 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 { + + /** + * Filters whether to use the web-vitals.js build with attribution. + * + * When using the attribution build of web-vitals, the metric object passed to report callbacks registered via + * `onTTFB`, `onFCP`, `onLCP`, `onCLS`, and `onINP` will include an additional {@link https://github.com/GoogleChrome/web-vitals#attribution attribution property}. + * For details, please refer to the {@link https://github.com/GoogleChrome/web-vitals web-vitals documentation}. + * + * For example, to opt in to using the attribution build: + * + * add_filter( 'od_use_web_vitals_attribution_build', '__return_true' ); + * + * Note that the attribution build is slightly larger than the standard build, so this is why it is not used by default. + * The additional attribution data is made available to client-side extension script modules registered via the `od_extension_module_urls` filter. + * + * @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', false ); + $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/docs/hooks.md b/plugins/optimization-detective/docs/hooks.md index 6b53bbd3d0..09e20bad44 100644 --- a/plugins/optimization-detective/docs/hooks.md +++ b/plugins/optimization-detective/docs/hooks.md @@ -53,6 +53,23 @@ The supplied context object includes these properties: ## Filters +### Filter: `od_use_web_vitals_attribution_build` (default: `false`) + +Filters whether to use the web-vitals.js build with attribution. + +When using the attribution build of web-vitals, the metric object passed to report callbacks registered via +`onTTFB`, `onFCP`, `onLCP`, `onCLS`, and `onINP` will include an additional [attribution property](https://github.com/GoogleChrome/web-vitals#attribution). +For details, please refer to the [web-vitals documentation](https://github.com/GoogleChrome/web-vitals). + +For example, to opt in to using the attribution build: + +```php +add_filter( 'od_use_web_vitals_attribution_build', '__return_true' ); +``` + +Note that the attribution build is slightly larger than the standard build, so this is why it is not used by default. +The additional attribution data is made available to client-side extension script modules registered via the `od_extension_module_urls` filter. + ### Filter: `od_breakpoint_max_widths` (default: `array(480, 600, 782)`) Filters the breakpoint max widths to group URL Metrics for various viewports. Each number represents the maximum width (inclusive) for a given breakpoint. So if there is one number, 480, then this means there will be two viewport groupings, one for 0\<=480, and another \>480. If instead there are the two breakpoints defined, 480 and 782, then this means there will be three viewport groups of URL Metrics, one for 0\<=480 (i.e. mobile), another 481\<=782 (i.e. phablet/tablet), and another \>782 (i.e. desktop). diff --git a/plugins/optimization-detective/tests/test-detection.php b/plugins/optimization-detective/tests/test-detection.php index a159591115..e78c7231d9 100644 --- a/plugins/optimization-detective/tests/test-detection.php +++ b/plugins/optimization-detective/tests/test-detection.php @@ -80,19 +80,20 @@ public function test_od_get_cache_purge_post_id( Closure $set_up, bool $expected /** * Data provider. * - * @return array}> + * @return array, expected_standard_build: bool}> */ public function data_provider_od_get_detection_script(): array { return array( 'unfiltered' => array( - 'set_up' => static function (): void {}, - 'expected_exports' => array( + 'set_up' => static function (): void {}, + 'expected_exports' => array( 'storageLockTTL' => MINUTE_IN_SECONDS, 'extensionModuleUrls' => array(), ), + 'expected_standard_build' => true, ), 'filtered' => array( - 'set_up' => static function (): void { + 'set_up' => static function (): void { add_filter( 'od_url_metric_storage_lock_ttl', static function (): int { @@ -106,11 +107,13 @@ static function ( array $urls ): array { return $urls; } ); + add_filter( 'od_use_web_vitals_attribution_build', '__return_true' ); }, - 'expected_exports' => array( + 'expected_exports' => array( 'storageLockTTL' => HOUR_IN_SECONDS, 'extensionModuleUrls' => array( home_url( '/my-extension.js', 'https' ) ), ), + 'expected_standard_build' => false, ), ); } @@ -122,10 +125,11 @@ static function ( array $urls ): array { * * @dataProvider data_provider_od_get_detection_script * - * @param Closure $set_up Set up callback. - * @param array}> $expected_exports Expected exports. + * @param Closure $set_up Set up callback. + * @param array $expected_exports Expected exports. + * @param bool $expected_standard_build Expected standard build. */ - public function test_od_get_detection_script_returns_script( Closure $set_up, array $expected_exports ): void { + public function test_od_get_detection_script_returns_script( Closure $set_up, array $expected_exports, bool $expected_standard_build ): void { $set_up(); $slug = od_get_url_metrics_slug( array( 'p' => '1' ) ); $current_etag = md5( '' ); @@ -140,6 +144,12 @@ public function test_od_get_detection_script_returns_script( Closure $set_up, ar foreach ( $expected_exports as $key => $value ) { $this->assertStringContainsString( sprintf( '%s:%s', wp_json_encode( $key ), wp_json_encode( $value ) ), $script ); } + $this->assertSame( 1, preg_match( '/"webVitalsLibrarySrc":("[^"]+?")/', $script, $matches ) ); + $web_vitals_library_src = json_decode( $matches[1] ); + $this->assertStringContainsString( + $expected_standard_build ? '/web-vitals.' : '/web-vitals-attribution.', + $web_vitals_library_src + ); $this->assertStringContainsString( '"minimumViewportWidth":0', $script ); $this->assertStringContainsString( '"minimumViewportWidth":481', $script ); $this->assertStringContainsString( '"minimumViewportWidth":601', $script ); diff --git a/plugins/optimization-detective/types.ts b/plugins/optimization-detective/types.ts index d92c532143..cf3e647eda 100644 --- a/plugins/optimization-detective/types.ts +++ b/plugins/optimization-detective/types.ts @@ -2,6 +2,13 @@ type ExcludeProps< T > = { [ k: string ]: any } & { [ K in keyof T ]?: never }; import { onTTFB, onFCP, onLCP, onINP, onCLS } from 'web-vitals'; +import { + onTTFB as onTTFBWithAttribution, + onFCP as onFCPWithAttribution, + onLCP as onLCPWithAttribution, + onINP as onINPWithAttribution, + onCLS as onCLSWithAttribution, +} from 'web-vitals/attribution'; export interface ElementData { isLCP: boolean; @@ -35,14 +42,19 @@ export type OnFCPFunction = typeof onFCP; export type OnLCPFunction = typeof onLCP; export type OnINPFunction = typeof onINP; export type OnCLSFunction = typeof onCLS; +export type OnTTFBWithAttributionFunction = typeof onTTFBWithAttribution; +export type OnFCPWithAttributionFunction = typeof onFCPWithAttribution; +export type OnLCPWithAttributionFunction = typeof onLCPWithAttribution; +export type OnINPWithAttributionFunction = typeof onINPWithAttribution; +export type OnCLSWithAttributionFunction = typeof onCLSWithAttribution; export type InitializeArgs = { readonly isDebug: boolean; - readonly onTTFB: OnTTFBFunction; - readonly onFCP: OnFCPFunction; - readonly onLCP: OnLCPFunction; - readonly onINP: OnINPFunction; - readonly onCLS: OnCLSFunction; + readonly onTTFB: OnTTFBFunction | OnTTFBWithAttributionFunction; + readonly onFCP: OnFCPFunction | OnFCPWithAttributionFunction; + readonly onLCP: OnLCPFunction | OnLCPWithAttributionFunction; + readonly onINP: OnINPFunction | OnINPWithAttributionFunction; + readonly onCLS: OnCLSFunction | OnCLSWithAttributionFunction; }; export type InitializeCallback = ( args: InitializeArgs ) => Promise< void >; diff --git a/webpack.config.js b/webpack.config.js index faaaa32677..21f6a43662 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`,