Next.js
Analytics & Monitoring in Next.js
Analytics & Monitoring in Next.js
Monitoring your Next.js application's performance, user behavior, and errors is crucial for maintaining a high-quality user experience. This lesson covers implementing analytics, error tracking, and performance monitoring in production applications.
Why Analytics & Monitoring Matter
Production monitoring helps you:
- Track user behavior: Understand how users interact with your application
- Identify performance bottlenecks: Find slow pages and API routes
- Catch errors early: Detect and fix issues before they affect users
- Make data-driven decisions: Use real metrics to guide development priorities
- Monitor Core Web Vitals: Track metrics that affect SEO and user experience
Vercel Analytics Integration
Vercel Analytics provides built-in performance monitoring for Next.js applications deployed on Vercel:
Install Vercel Analytics:
npm install @vercel/analytics
app/layout.tsx:
import { Analytics } from '@vercel/analytics/react';
import { SpeedInsights } from '@vercel/speed-insights/next';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
<SpeedInsights />
</body>
</html>
);
}
Tip: The Analytics component automatically tracks page views, Web Vitals (LCP, FID, CLS), and other performance metrics without requiring additional configuration.
Custom Event Tracking
Track custom events to understand specific user interactions:
components/ProductCard.tsx:
'use client';
import { track } from '@vercel/analytics';
export default function ProductCard({ product }: { product: Product }) {
const handleAddToCart = () => {
// Track custom event
track('add_to_cart', {
product_id: product.id,
product_name: product.name,
price: product.price,
category: product.category,
});
// Add to cart logic
addToCart(product);
};
return (
<div className="product-card">
<h3>{product.name}</h3>
<p>${product.price}</p>
<button onClick={handleAddToCart}>
Add to Cart
</button>
</div>
);
}
Common custom events:
// Search tracking
track('search', {
query: searchTerm,
results_count: results.length,
});
// Feature usage
track('feature_used', {
feature_name: 'dark_mode',
enabled: isDarkMode,
});
// Conversion tracking
track('purchase_completed', {
order_id: order.id,
total: order.total,
items_count: order.items.length,
});
// Engagement tracking
track('video_played', {
video_id: video.id,
duration: video.duration,
timestamp: currentTime,
});
Google Analytics 4 Integration
Integrate GA4 for comprehensive analytics:
Install GA library:
npm install @next/third-parties
app/layout.tsx:
import { GoogleAnalytics } from '@next/third-parties/google';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<GoogleAnalytics gaId="G-XXXXXXXXXX" />
</body>
</html>
);
}
lib/analytics.ts (custom events):
export const GA_TRACKING_ID = process.env.NEXT_PUBLIC_GA_ID;
// Track page views
export const pageview = (url: string) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('config', GA_TRACKING_ID, {
page_path: url,
});
}
};
// Track events
export const event = ({
action,
category,
label,
value,
}: {
action: string;
category: string;
label: string;
value?: number;
}) => {
if (typeof window !== 'undefined' && window.gtag) {
window.gtag('event', action, {
event_category: category,
event_label: label,
value: value,
});
}
};
Usage in components:
'use client';
import { event } from '@/lib/analytics';
export default function NewsletterForm() {
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
// Track event
event({
action: 'submit',
category: 'Newsletter',
label: 'Subscription Form',
});
// Submit logic
await subscribeToNewsletter(email);
};
return (
<form onSubmit={handleSubmit}>
{/* form fields */}
</form>
);
}
Error Tracking with Sentry
Sentry provides comprehensive error tracking and performance monitoring:
Install Sentry:
npx @sentry/wizard@latest -i nextjs
sentry.client.config.ts:
import * as Sentry from '@sentry/nextjs';
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// Performance monitoring
tracesSampleRate: 1.0,
// Session replay
replaysSessionSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
// Environment
environment: process.env.NODE_ENV,
// Release tracking
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});
app/error.tsx (error boundary):
'use client';
import * as Sentry from '@sentry/nextjs';
import { useEffect } from 'react';
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string };
reset: () => void;
}) {
useEffect(() => {
// Log error to Sentry
Sentry.captureException(error);
}, [error]);
return (
<div className="error-container">
<h2>Something went wrong!</h2>
<button onClick={reset}>Try again</button>
</div>
);
}
Manual error tracking:
import * as Sentry from '@sentry/nextjs';
async function fetchUserData(userId: string) {
try {
const response = await fetch(\`/api/users/${userId}\`);
if (!response.ok) {
throw new Error(\`Failed to fetch user: ${response.status}\`);
}
return await response.json();
} catch (error) {
// Capture error with context
Sentry.captureException(error, {
tags: {
section: 'user_data',
},
extra: {
userId,
url: \`/api/users/${userId}\`,
},
});
throw error;
}
}
Performance Monitoring
Monitor Core Web Vitals and custom performance metrics:
app/layout.tsx:
import { WebVitals } from './web-vitals';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<WebVitals />
</body>
</html>
);
}
app/web-vitals.tsx:
'use client';
import { useReportWebVitals } from 'next/web-vitals';
export function WebVitals() {
useReportWebVitals((metric) => {
// Log to console in development
if (process.env.NODE_ENV === 'development') {
console.log(metric);
}
// Send to analytics service
const body = JSON.stringify({
name: metric.name,
value: metric.value,
rating: metric.rating,
delta: metric.delta,
id: metric.id,
});
// Send to your analytics endpoint
if (navigator.sendBeacon) {
navigator.sendBeacon('/api/analytics/web-vitals', body);
} else {
fetch('/api/analytics/web-vitals', {
method: 'POST',
body,
keepalive: true,
});
}
});
return null;
}
app/api/analytics/web-vitals/route.ts:
import { NextRequest, NextResponse } from 'next/server';
export async function POST(request: NextRequest) {
const metric = await request.json();
// Store in database or send to analytics service
console.log('Web Vital:', metric);
// Example: Send to external service
if (process.env.ANALYTICS_API_KEY) {
await fetch('https://analytics.example.com/vitals', {
method: 'POST',
headers: {
'Authorization': \`Bearer ${process.env.ANALYTICS_API_KEY}\`,
'Content-Type': 'application/json',
},
body: JSON.stringify(metric),
});
}
return NextResponse.json({ success: true });
}
Custom Performance Marks
Track custom performance metrics:
lib/performance.ts:
export class PerformanceTracker {
private marks: Map<string, number> = new Map();
mark(name: string) {
if (typeof window !== 'undefined') {
performance.mark(name);
this.marks.set(name, performance.now());
}
}
measure(name: string, startMark: string, endMark?: string) {
if (typeof window !== 'undefined') {
try {
performance.measure(name, startMark, endMark);
const measure = performance.getEntriesByName(name)[0];
// Send to analytics
this.sendMetric({
name,
duration: measure.duration,
startTime: measure.startTime,
});
return measure.duration;
} catch (error) {
console.error('Performance measurement failed:', error);
}
}
return 0;
}
private sendMetric(metric: any) {
// Send to your analytics service
fetch('/api/analytics/performance', {
method: 'POST',
body: JSON.stringify(metric),
keepalive: true,
});
}
}
export const performanceTracker = new PerformanceTracker();
Usage example:
'use client';
import { performanceTracker } from '@/lib/performance';
import { useEffect } from 'react';
export default function Dashboard() {
useEffect(() => {
performanceTracker.mark('dashboard-render-start');
// Simulate data loading
fetchDashboardData().then(() => {
performanceTracker.mark('dashboard-data-loaded');
performanceTracker.measure(
'dashboard-load-time',
'dashboard-render-start',
'dashboard-data-loaded'
);
});
}, []);
return (
<div className="dashboard">
{/* dashboard content */}
</div>
);
}
Real User Monitoring (RUM)
Track real user experience metrics:
lib/rum.ts:
interface RUMMetrics {
url: string;
userAgent: string;
connection?: string;
deviceMemory?: number;
navigationTiming: {
dns: number;
tcp: number;
request: number;
response: number;
domLoading: number;
domInteractive: number;
domComplete: number;
loadComplete: number;
};
}
export function collectRUMMetrics(): RUMMetrics | null {
if (typeof window === 'undefined') return null;
const nav = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming;
if (!nav) return null;
return {
url: window.location.href,
userAgent: navigator.userAgent,
connection: (navigator as any).connection?.effectiveType,
deviceMemory: (navigator as any).deviceMemory,
navigationTiming: {
dns: nav.domainLookupEnd - nav.domainLookupStart,
tcp: nav.connectEnd - nav.connectStart,
request: nav.responseStart - nav.requestStart,
response: nav.responseEnd - nav.responseStart,
domLoading: nav.domInteractive - nav.responseEnd,
domInteractive: nav.domInteractive - nav.navigationStart,
domComplete: nav.domComplete - nav.navigationStart,
loadComplete: nav.loadEventEnd - nav.navigationStart,
},
};
}
export function sendRUMMetrics() {
window.addEventListener('load', () => {
setTimeout(() => {
const metrics = collectRUMMetrics();
if (metrics) {
navigator.sendBeacon(
'/api/analytics/rum',
JSON.stringify(metrics)
);
}
}, 0);
});
}
Monitoring Dashboard Example
Create a custom analytics dashboard:
app/admin/analytics/page.tsx:
import { Suspense } from 'react';
import { getAnalytics } from '@/lib/analytics-data';
export default async function AnalyticsDashboard() {
const analytics = await getAnalytics({
range: '7d',
});
return (
<div className="analytics-dashboard">
<h1>Analytics Dashboard</h1>
<div className="metrics-grid">
<MetricCard
title="Page Views"
value={analytics.pageViews}
change={analytics.pageViewsChange}
/>
<MetricCard
title="Unique Visitors"
value={analytics.uniqueVisitors}
change={analytics.visitorsChange}
/>
<MetricCard
title="Avg. Session Duration"
value={\`${analytics.avgSessionDuration}s\`}
change={analytics.sessionChange}
/>
<MetricCard
title="Bounce Rate"
value={\`${analytics.bounceRate}%\`}
change={analytics.bounceRateChange}
inverse
/>
</div>
<div className="charts-grid">
<Suspense fallback={<Loading />}>
<PageViewsChart data={analytics.pageViewsOverTime} />
</Suspense>
<Suspense fallback={<Loading />}>
<TopPagesTable pages={analytics.topPages} />
</Suspense>
<Suspense fallback={<Loading />}>
<WebVitalsChart vitals={analytics.webVitals} />
</Suspense>
</div>
</div>
);
}
Exercise:
- Install and configure Vercel Analytics in a Next.js project
- Add custom event tracking for 3 key user interactions
- Set up Sentry error tracking with custom context
- Create a custom performance tracking system
- Build a simple analytics dashboard showing key metrics
Best Practices:
- Track only meaningful events - avoid tracking noise
- Sample high-traffic applications to reduce costs
- Set up alerts for critical errors and performance degradation
- Respect user privacy - anonymize sensitive data
- Use environment variables for all API keys
- Monitor both frontend and backend performance
- Regularly review and act on analytics data