Next.js

Analytics & Monitoring in Next.js

40 min Lesson 36 of 40

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:
  1. Install and configure Vercel Analytics in a Next.js project
  2. Add custom event tracking for 3 key user interactions
  3. Set up Sentry error tracking with custom context
  4. Create a custom performance tracking system
  5. 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