diff --git a/docs/advanced-features/measuring-performance.md b/docs/advanced-features/measuring-performance.md index 767833f8fae54..b9c331d6312fd 100644 --- a/docs/advanced-features/measuring-performance.md +++ b/docs/advanced-features/measuring-performance.md @@ -181,6 +181,37 @@ export function reportWebVitals(metric) { > > Read more about [sending results to Google Analytics](https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics). +## Web Vitals Attribution + +When debugging issues related to Web Vitals, it is often helpful if we can pinpoint the source of the problem. +For example, in the case of Cumulative Layout Shift (CLS), we might want to know the first element that shifted when the single largest layout shift occurred. +Or, in the case of Largest Contentful Paint (LCP), we might want to identify the element corresponding to the LCP for the page. +If the LCP element is an image, knowing the URL of the image resource can help us locate the asset we need to optimize. + +Pinpointing the biggest contributor to the Web Vitals score, aka [attribution](https://github.com/GoogleChrome/web-vitals/blob/4ca38ae64b8d1e899028c692f94d4c56acfc996c/README.md#attribution), +allows us to obtain more in-depth information like entries for [PerformanceEventTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceEventTiming), [PerformanceNavigationTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceNavigationTiming) and [PerformanceResourceTiming](https://developer.mozilla.org/en-US/docs/Web/API/PerformanceResourceTiming). + +Attribution is disabled by default in Next.js but can be enabled **per metric** by specifying a `config` for `reportWebVitals()`. + +```js +// pages/_app.js +export function reportWebVitals(metric) { + console.log(metric) +} + +reportWebVitals.config = { + attributions: ['CLS', 'LCP'], +} + +function MyApp({ Component, pageProps }) { + return +} + +export default MyApp +``` + +Valid attribution values are all `web-vitals` metrics specified in the [`NextWebVitalsMetric`](https://github.com/vercel/next.js/blob/442378d21dd56d6e769863eb8c2cb521a463a2e0/packages/next/shared/lib/utils.ts#L43) type. + ## TypeScript If you are using TypeScript, you can use the built-in type `NextWebVitalsMetric`: diff --git a/packages/next/client/index.tsx b/packages/next/client/index.tsx index c0f5d8be03373..c17d5d24f5d5f 100644 --- a/packages/next/client/index.tsx +++ b/packages/next/client/index.tsx @@ -26,7 +26,9 @@ import { import { Portal } from './portal' import initHeadManager from './head-manager' import PageLoader, { StyleSheetTuple } from './page-loader' -import measureWebVitals from './performance-relayer' +import measureWebVitals, { + setPerformanceRelayerConfig, +} from './performance-relayer' import { RouteAnnouncer } from './route-announcer' import { createRouter, makePublicRouterInstance } from './router' import { getProperError } from '../lib/is-error' @@ -765,6 +767,9 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { const { component: app, exports: mod } = appEntrypoint CachedApp = app as AppComponent if (mod && mod.reportWebVitals) { + if (mod.reportWebVitals.config) { + setPerformanceRelayerConfig(mod.reportWebVitals.config) + } onPerfEntry = ({ id, name, @@ -773,6 +778,7 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { duration, entryType, entries, + attribution, }: any): void => { // Combines timestamp with random number for unique ID const uniqueID: string = `${Date.now()}-${ @@ -794,6 +800,9 @@ export async function hydrate(opts?: { beforeRender?: () => Promise }) { ? 'custom' : 'web-vital', } + if (attribution) { + webVitals.attribution = attribution + } mod.reportWebVitals(webVitals) } } diff --git a/packages/next/client/performance-relayer.ts b/packages/next/client/performance-relayer.ts index 685b9289c78a2..0a541574b74e4 100644 --- a/packages/next/client/performance-relayer.ts +++ b/packages/next/client/performance-relayer.ts @@ -1,18 +1,15 @@ /* global location */ -import { - onCLS, - onFCP, - onFID, - onINP, - onLCP, - onTTFB, - Metric, - ReportCallback, -} from 'next/dist/compiled/web-vitals' +import { Metric, ReportCallback } from 'next/dist/compiled/web-vitals' +interface PerformanceRelayerConfig { + attributions: Array +} + +const WEB_VITALS = ['CLS', 'FCP', 'FID', 'INP', 'LCP', 'TTFB'] as const const initialHref = location.href let isRegistered = false let userReportHandler: ReportCallback | undefined +let config: PerformanceRelayerConfig | undefined function onReport(metric: Metric): void { if (userReportHandler) { @@ -82,10 +79,21 @@ export default (onPerfEntry?: ReportCallback): void => { } isRegistered = true - onCLS(onReport) - onFID(onReport) - onFCP(onReport) - onLCP(onReport) - onTTFB(onReport) - onINP(onReport) + for (const webVital of WEB_VITALS) { + const m = config?.attributions.includes(webVital) + ? require('next/dist/compiled/web-vitals-attribution') + : require('next/dist/compiled/web-vitals') + m[`on${webVital}`](onReport) + } +} + +export function setPerformanceRelayerConfig( + input: PerformanceRelayerConfig +): void { + const attributions = Array.isArray(input.attributions) + ? input.attributions + : [] + config = { + attributions: attributions.filter((attr) => WEB_VITALS.includes(attr)), + } } diff --git a/packages/next/compiled/web-vitals-attribution/LICENSE b/packages/next/compiled/web-vitals-attribution/LICENSE new file mode 100644 index 0000000000000..618d629b34698 --- /dev/null +++ b/packages/next/compiled/web-vitals-attribution/LICENSE @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2020 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/next/compiled/web-vitals-attribution/package.json b/packages/next/compiled/web-vitals-attribution/package.json new file mode 100644 index 0000000000000..27400e845977a --- /dev/null +++ b/packages/next/compiled/web-vitals-attribution/package.json @@ -0,0 +1 @@ +{"name":"web-vitals","main":"web-vitals.attribution.js","author":{"name":"Philip Walton","email":"philip@philipwalton.com","url":"http://philipwalton.com"},"license":"Apache-2.0"} diff --git a/packages/next/compiled/web-vitals-attribution/web-vitals.attribution.js b/packages/next/compiled/web-vitals-attribution/web-vitals.attribution.js new file mode 100644 index 0000000000000..9aa0d5afd6b99 --- /dev/null +++ b/packages/next/compiled/web-vitals-attribution/web-vitals.attribution.js @@ -0,0 +1 @@ +(function(){"use strict";var t={};!function(){t.d=function(g,b){for(var C in b){if(t.o(b,C)&&!t.o(g,C)){Object.defineProperty(g,C,{enumerable:true,get:b[C]})}}}}();!function(){t.o=function(t,g){return Object.prototype.hasOwnProperty.call(t,g)}}();!function(){t.r=function(t){if(typeof Symbol!=="undefined"&&Symbol.toStringTag){Object.defineProperty(t,Symbol.toStringTag,{value:"Module"})}Object.defineProperty(t,"__esModule",{value:true})}}();if(typeof t!=="undefined")t.ab=__dirname+"/";var g={};t.r(g);t.d(g,{onCLS:function(){return w},onFCP:function(){return L},onFID:function(){return D},onINP:function(){return J},onLCP:function(){return Q},onTTFB:function(){return Y}});var b,C,F,P,A,a=function(){return window.performance&&performance.getEntriesByType&&performance.getEntriesByType("navigation")[0]},o=function(t){if("loading"===document.readyState)return"loading";var g=a();if(g){if(t(g||100)-1)return b||F;if(b=b?F+">"+b:F,C.id)break;t=C.parentNode}}catch(t){}return b},N=-1,f=function(){return N},l=function(t){addEventListener("pageshow",(function(g){g.persisted&&(N=g.timeStamp,t(g))}),!0)},d=function(){var t=a();return t&&t.activationStart||0},m=function(t,g){var b=a(),C="navigate";return f()>=0?C="back-forward-cache":b&&(C=document.prerendering||d()>0?"prerender":b.type.replace(/_/g,"-")),{name:t,value:void 0===g?-1:g,rating:"good",delta:0,entries:[],id:"v3-".concat(Date.now(),"-").concat(Math.floor(8999999999999*Math.random())+1e12),navigationType:C}},v=function(t,g,b){try{if(PerformanceObserver.supportedEntryTypes.includes(t)){var C=new PerformanceObserver((function(t){g(t.getEntries())}));return C.observe(Object.assign({type:t,buffered:!0},b||{})),C}}catch(t){}},p=function(t,g){var b=function n(b){"pagehide"!==b.type&&"hidden"!==document.visibilityState||(t(b),g&&(removeEventListener("visibilitychange",n,!0),removeEventListener("pagehide",n,!0)))};addEventListener("visibilitychange",b,!0),addEventListener("pagehide",b,!0)},h=function(t,g,b,C){var F,P;return function(A){g.value>=0&&(A||C)&&((P=g.value-(F||0))||void 0===F)&&(F=g.value,g.delta=P,g.rating=function(t,g){return t>g[1]?"poor":t>g[0]?"needs-improvement":"good"}(g.value,b),t(g))}},q=-1,T=function(){return"hidden"!==document.visibilityState||document.prerendering?1/0:0},y=function(){p((function(t){var g=t.timeStamp;q=g}),!0)},E=function(){return q<0&&(q=T(),y(),l((function(){setTimeout((function(){q=T(),y()}),0)}))),{get firstHiddenTime(){return q}}},S=function(t,g){g=g||{};var b,C=[1800,3e3],F=E(),P=m("FCP"),o=function(t){t.forEach((function(t){"first-contentful-paint"===t.name&&(N&&N.disconnect(),t.startTime-1&&t(g)},F=m("CLS",0),P=0,A=[],c=function(t){t.forEach((function(t){if(!t.hadRecentInput){var g=A[0],b=A[A.length-1];P&&t.startTime-b.startTime<1e3&&t.startTime-g.startTime<5e3?(P+=t.value,A.push(t)):(P=t.value,A=[t]),P>F.value&&(F.value=P,F.entries=A,C())}}))},N=v("layout-shift",c);N&&(C=h(i,F,b,g.reportAllChanges),p((function(){c(N.takeRecords()),C(!0)})),l((function(){P=0,_=-1,F=m("CLS",0),C=h(i,F,b,g.reportAllChanges)})))}((function(g){!function(t){if(t.entries.length){var g=t.entries.reduce((function(t,g){return t&&t.value>g.value?t:g}));if(g&&g.sources&&g.sources.length){var b=(C=g.sources).find((function(t){return t.node&&1===t.node.nodeType}))||C[0];b&&(t.attribution={largestShiftTarget:c(b.node),largestShiftTime:g.startTime,largestShiftValue:g.value,largestShiftSource:b,largestShiftEntry:g,loadState:o(g.startTime)})}}else t.attribution={};var C}(g),t(g)}),g)},L=function(t,g){S((function(g){!function(t){if(t.entries.length){var g=a(),b=t.entries[t.entries.length-1];if(g){var C=g.activationStart||0,F=Math.max(0,g.responseStart-C);t.attribution={timeToFirstByte:F,firstByteToFCP:t.value-F,loadState:o(t.entries[0].startTime),navigationEntry:g,fcpEntry:b}}}else t.attribution={timeToFirstByte:0,firstByteToFCP:t.value,loadState:o(f())}}(g),t(g)}),g)},V={passive:!0,capture:!0},K=new Date,M=function(t,g){b||(b=g,C=t,F=new Date,I(removeEventListener),B())},B=function(){if(C>=0&&C1e12?new Date:performance.now())-t.timeStamp;"pointerdown"==t.type?function(t,g){var n=function(){M(t,g),i()},r=function(){i()},i=function(){removeEventListener("pointerup",n,V),removeEventListener("pointercancel",r,V)};addEventListener("pointerup",n,V),addEventListener("pointercancel",r,V)}(g,t):M(g,t)}},I=function(t){["mousedown","keydown","touchstart","pointerdown"].forEach((function(g){return t(g,x,V)}))},k=function(t,g){g=g||{};var F,A=[100,300],N=E(),q=m("FID"),s=function(t){t.startTimeg.latency){if(b)b.entries.push(t),b.latency=Math.max(b.latency,t.duration);else{var C={id:t.interactionId,latency:t.duration,entries:[t]};ne[C.id]=C,te.push(C)}te.sort((function(t,g){return g.latency-t.latency})),te.splice(10).forEach((function(t){delete ne[t.id]}))}},G=function(t,g){g=g||{};var b=[200,500];O();var C,F=m("INP"),a=function(t){t.forEach((function(t){(t.interactionId&&z(t),"first-input"===t.entryType)&&(!te.some((function(g){return g.entries.some((function(g){return t.duration===g.duration&&t.startTime===g.startTime}))}))&&z(t))}));var g,b=(g=Math.min(te.length-1,Math.floor(U()/50)),te[g]);b&&b.latency!==F.value&&(F.value=b.latency,F.entries=b.entries,C())},P=v("event",a,{durationThreshold:g.durationThreshold||40});C=h(t,F,b,g.reportAllChanges),P&&(P.observe({type:"first-input",buffered:!0}),p((function(){a(P.takeRecords()),F.value<0&&U()>0&&(F.value=0,F.entries=[]),C(!0)})),l((function(){te=[],ee=H(),F=m("INP"),C=h(t,F,b,g.reportAllChanges)})))},J=function(t,g){G((function(g){!function(t){if(t.entries.length){var g=t.entries.sort((function(t,g){return g.duration-t.duration||g.processingEnd-g.processingStart-(t.processingEnd-t.processingStart)}))[0];t.attribution={eventTarget:c(g.target),eventType:g.name,eventTime:g.startTime,eventEntry:g,loadState:o(g.startTime)}}else t.attribution={}}(g),t(g)}),g)},re={},Q=function(t,g){!function(t,g){g=g||{};var b,C=[2500,4e3],F=E(),P=m("LCP"),o=function(t){var g=t[t.length-1];if(g){var C=g.startTime-d();Cperformance.now())return;C.entries=[P],F(!0),l((function(){C=m("TTFB",0),(F=h(t,C,b,g.reportAllChanges))(!0)}))}}))},Y=function(t,g){X((function(g){!function(t){if(t.entries.length){var g=t.entries[0],b=g.activationStart||0,C=Math.max(g.domainLookupStart-b,0),F=Math.max(g.connectStart-b,0),P=Math.max(g.requestStart-b,0);t.attribution={waitingTime:C,dnsTime:F-C,connectionTime:P-F,requestTime:t.value-P,navigationEntry:g}}else t.attribution={waitingTime:0,dnsTime:0,connectionTime:0,requestTime:0}}(g),t(g)}),g)};module.exports=g})(); \ No newline at end of file diff --git a/packages/next/shared/lib/utils.ts b/packages/next/shared/lib/utils.ts index 67c486a83d1e4..68fd05098c87c 100644 --- a/packages/next/shared/lib/utils.ts +++ b/packages/next/shared/lib/utils.ts @@ -45,6 +45,7 @@ export type NextWebVitalsMetric = { id: string startTime: number value: number + attribution?: { [key: string]: unknown } } & ( | { label: 'web-vital' diff --git a/packages/next/taskfile.js b/packages/next/taskfile.js index b91bf3be9ffa6..f57238f4d80d2 100644 --- a/packages/next/taskfile.js +++ b/packages/next/taskfile.js @@ -1636,6 +1636,27 @@ export async function ncc_web_vitals(task, opts) { .target('compiled/web-vitals') } // eslint-disable-next-line camelcase +externals['web-vitals-attribution'] = + 'next/dist/compiled/web-vitals-attribution' +export async function ncc_web_vitals_attribution(task, opts) { + await task + .source( + opts.src || + relative( + __dirname, + resolve(require.resolve('web-vitals'), '../web-vitals.attribution.js') + ) + ) + .ncc({ + packageName: 'web-vitals', + bundleName: 'web-vitals-attribution', + externals, + target: 'es5', + esm: false, + }) + .target('compiled/web-vitals-attribution') +} +// eslint-disable-next-line camelcase externals['webpack-sources'] = 'error webpack-sources version not specified' externals['webpack-sources1'] = 'next/dist/compiled/webpack-sources1' export async function ncc_webpack_sources1(task, opts) { @@ -1900,6 +1921,7 @@ export async function ncc(task, opts) { 'ncc_text_table', 'ncc_unistore', 'ncc_web_vitals', + 'ncc_web_vitals_attribution', 'ncc_webpack_bundle5', 'ncc_webpack_sources1', 'ncc_webpack_sources3', diff --git a/test/integration/relay-analytics/pages/_app.js b/test/integration/relay-analytics/pages/_app.js index 44f936f124f05..26818c660bca7 100644 --- a/test/integration/relay-analytics/pages/_app.js +++ b/test/integration/relay-analytics/pages/_app.js @@ -16,4 +16,12 @@ export function reportWebVitals(data) { ) const countMap = window.__BEACONS_COUNT countMap.set(name, (countMap.get(name) || 0) + 1) + + if (data.attribution) { + ;(window.__metricsWithAttribution ??= []).push(data) + } +} + +reportWebVitals.config = { + attributions: ['LCP'], } diff --git a/test/integration/relay-analytics/test/index.test.js b/test/integration/relay-analytics/test/index.test.js index 5ee12ffa9e39a..17e447d5ae057 100644 --- a/test/integration/relay-analytics/test/index.test.js +++ b/test/integration/relay-analytics/test/index.test.js @@ -128,4 +128,21 @@ function runTest() { }, 'success') await browser.close() }) + + it('reports attribution', async () => { + const browser = await webdriver(appPort, '/') + // trigger paint + await browser.elementByCss('button').click() + await browser.waitForCondition( + `window.__metricsWithAttribution?.length > 0` + ) + const str = await browser.eval( + `JSON.stringify(window.__metricsWithAttribution)` + ) + const metrics = JSON.parse(str) + const LCP = metrics.find((m) => m.name === 'LCP') + expect(LCP).toBeDefined() + expect(LCP.attribution).toBeDefined() + expect(LCP.attribution.element).toBe('#__next>div>h1') + }) }