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')
+ })
}