diff --git a/packages/next/client/app-index.tsx b/packages/next/client/app-index.tsx
index fa475942a2f96..552b2c00f7398 100644
--- a/packages/next/client/app-index.tsx
+++ b/packages/next/client/app-index.tsx
@@ -5,6 +5,8 @@ import ReactDOMClient from 'react-dom/client'
import React from 'react'
import { createFromReadableStream } from 'next/dist/compiled/react-server-dom-webpack'
+import measureWebVitals from './performance-relayer'
+
///
// Override chunk URL mapping in the webpack runtime
@@ -151,6 +153,10 @@ function ServerRoot({ cacheKey }: { cacheKey: string }) {
}
function Root({ children }: React.PropsWithChildren<{}>): React.ReactElement {
+ React.useEffect(() => {
+ measureWebVitals()
+ }, [])
+
if (process.env.__NEXT_TEST_MODE) {
// eslint-disable-next-line react-hooks/rules-of-hooks
React.useEffect(() => {
diff --git a/packages/next/client/performance-relayer.ts b/packages/next/client/performance-relayer.ts
index c1d87fed8784b..685b9289c78a2 100644
--- a/packages/next/client/performance-relayer.ts
+++ b/packages/next/client/performance-relayer.ts
@@ -32,7 +32,7 @@ function onReport(metric: Metric): void {
const body: Record = {
dsn: process.env.__NEXT_ANALYTICS_ID,
id: metric.id,
- page: window.__NEXT_DATA__.page,
+ page: window.__NEXT_DATA__?.page,
href: initialHref,
event_name: metric.name,
value: metric.value.toString(),
diff --git a/test/e2e/app-dir/index.test.ts b/test/e2e/app-dir/index.test.ts
index 5d62435fb27d0..fcc5c80c7adea 100644
--- a/test/e2e/app-dir/index.test.ts
+++ b/test/e2e/app-dir/index.test.ts
@@ -29,6 +29,9 @@ describe('app dir', () => {
'react-dom': 'experimental',
},
skipStart: true,
+ env: {
+ VERCEL_ANALYTICS_ID: 'fake-analytics-id',
+ },
})
if (assetPrefix) {
@@ -1454,6 +1457,40 @@ describe('app dir', () => {
})
})
+ // Analytics events are only sent in production
+ ;(isDev ? describe.skip : describe)('Vercel analytics', () => {
+ it('should send web vitals to Vercel analytics', async () => {
+ let eventsCount = 0
+ let countEvents = false
+ const browser = await webdriver(next.url, '/client-nested', {
+ beforePageLoad(page) {
+ page.route(
+ 'https://vitals.vercel-insights.com/v1/vitals',
+ (route) => {
+ if (countEvents) {
+ eventsCount += 1
+ }
+
+ route.fulfill()
+ }
+ )
+ },
+ })
+
+ // Start counting analytics events
+ countEvents = true
+
+ // Refresh will trigger CLS and LCP. When page loads FCP and TTFB will trigger:
+ await browser.refresh()
+
+ // After interaction LCP and FID will trigger
+ await browser.elementByCss('button').click()
+
+ // Make sure all registered events in performance-relayer has fired
+ await check(() => eventsCount, /6/)
+ })
+ })
+
describe('known bugs', () => {
it('should not share flight data between requests', async () => {
const fetches = await Promise.all(