Integrate analytics, error tracking, and performance monitoring into your Next.js app.
useReportWebVitals from next/vitals to capture Core Web Vitals in App Router."use client";
import { useReportWebVitals } from "next/vitals";
export function WebVitals() {
useReportWebVitals((metric) => {
console.log(metric); // { name, value, id, ... }
});
return null;
}
// Place <WebVitals /> in app/layout.tsximport { getCLS, getINP, getLCP } from "web-vitals";
function sendToAnalytics({ name, value, id }) {
fetch("/api/vitals", {
method: "POST",
body: JSON.stringify({ name, value, id })
});
}
getCLS(sendToAnalytics);
getINP(sendToAnalytics);
getLCP(sendToAnalytics);reportWebVitals from pages/_app.js.// pages/_app.js — Pages Router only
export function reportWebVitals(metric) {
console.log(metric); // { name, value, id, startTime, ... }
}npm install @sentry/nextjs// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig({
// your Next.js config
}, {
org: "your-org",
project: "your-project",
authToken: process.env.SENTRY_AUTH_TOKEN
});import * as Sentry from "@sentry/nextjs";
Sentry.captureException(error);// app/error.tsx
"use client";
import * as Sentry from "@sentry/nextjs";
import { useEffect } from "react";
export default function Error({ error, reset }) {
useEffect(() => {
Sentry.captureException(error);
}, [error]);
return <button onClick={reset}>Try again</button>;
}Sentry.setUser({ id: user.id, email: user.email });// app/layout.tsx
import Script from "next/script";
export default function RootLayout({ children }) {
return (
<html>
<head>
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
/></Script>
</head>
<body>{children}</body>
</html>
);
}function trackEvent(name: string) {
window.gtag('event', name);
}"use client";
import { usePathname } from "next/navigation";
import { useEffect } from "react";
export function Analytics() {
const pathname = usePathname();
useEffect(() => {
window.gtag?.("event", "page_view", { page_path: pathname });
}, [pathname]);
return null;
}function trackPurchase(order) {
window.gtag("event", "purchase", {
transaction_id: order.id,
value: order.total,
currency: "USD"
});
}<Script
src="https://www.googletagmanager.com/gtm.js?id=GTM-XXXX"
strategy="afterInteractive"
/>// Track API response times
export async function POST(request: Request) {
const start = performance.now();
try {
const result = await processRequest(request);
const duration = performance.now() - start;
// Log metric
console.log(`Request took ${duration}ms`);
return Response.json(result);
} catch (error) {
Sentry.captureException(error);
return Response.json({ error: "Failed" }, { status: 500 });
}
}async function recordMetric(name: string, value: number) {
await fetch("/api/metrics", {
method: "POST",
body: JSON.stringify({ name, value })
});
}// app/api/metrics/route.ts
export async function POST(request: Request) {
const { name, value } = await request.json();
await db.metric.create({ data: { name, value, at: new Date() } });
return Response.json({ ok: true });
}function trackSearch(query: string) {
fetch("/api/metrics", {
method: "POST",
body: JSON.stringify({ name: "search", value: query.length })
});
}performance.mark("component-start");
// ... render
performance.mark("component-end");
performance.measure("component", "component-start", "component-end");npm install -g @lhci/cli@*
lhci autorun{
"scripts": {
"analyze": "ANALYZE=true npm run build"
}
}// next.config.js
const withBundleAnalyzer = require("@next/bundle-analyzer")({
enabled: process.env.ANALYZE === "true"
});- name: Run Lighthouse CI
run: lhci autorun
env:
LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_TOKEN }}{
"assert": {
"assertions": {
"categories:performance": ["error", { "minScore": 0.9 }]
}
}
}In the App Router, import useReportWebVitals from next/vitals and call it inside a Client Component placed in your root layout — Next.js passes each metric (CLS, INP, LCP, FCP, TTFB) to your callback. For the Pages Router, export a reportWebVitals function from pages/_app.js instead.
Install @sentry/nextjs and run npx @sentry/wizard@latest -i nextjs to auto-configure sentry.client.config.js, sentry.server.config.js, and sentry.edge.config.js. The wizard also wraps your next.config.js with withSentryConfig to enable source map uploads for readable stack traces.
Yes — create a client component that loads the gtag script via next/script with strategy='afterInteractive' and place it in your root layout. Call gtag('event', ...) for custom events, and use usePathname with a useEffect to fire page view events on route changes.
reportWebVitals captures browser-measured paint and layout metrics (CLS, LCP, etc.) that reflect perceived page speed, while Sentry performance monitoring traces request durations, API calls, and server-side spans across the full stack. Use both together to correlate slow backend operations with poor front-end user experience scores.
Use the performance.mark() and performance.measure() Web APIs in client components to instrument specific interactions, then read results with performance.getEntriesByType('measure') and send them to your own /api/metrics endpoint. For server-side timing, use Date.now() around data-fetching logic inside Server Components or Route Handlers and log to your observability backend.