Next.js course Β· Module 8: Deployment and Production
Performance monitoring and error analytics
In this lesson12
In production Next.js applications, performance monitoring and error tracking are key to maintaining high quality of delivered services. In this module, we will learn about tools and techniques that will allow us to effectively monitor the application, detect problems before users do, and optimize performance on an ongoing basis.
The importance of monitoring in production applications
Monitoring is the process of continuously observing an application in order to:
- Early detection of problems - before they affect users
- Performance tracking - loading times, Core Web Vitals, throughput
- Analysis of user behavior - how users use the application
- Identifying bottlenecks - places requiring optimization
- Ensuring SLA - meeting agreed service levels
Types of monitoring
1. Performance Monitoring
- Core Web Vitals (LCP, FID, CLS)
- Page loading times
- API response times
- Resource consumption (CPU, memory)
2. Error Monitoring
- JavaScript errors
- API errors
- Network errors
- Stack traces
3. Infrastructure monitoring
- Server availability
- Resource consumption
- Network metrics
4. User Experience Monitoring (UX)
- Real User Monitoring (RUM)
- Session recordings
- Heat maps
- User flows
Integration with Sentry for error monitoring
Sentry is one of the most popular tools for error monitoring in JavaScript and Next.js applications.
1. Installation and configuration of Sentry
1npm install @sentry/nextjs
2# or
3yarn add @sentry/nextjsCreate the configuration files:
1// sentry.client.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6
7 // Environment settings
8 environment: process.env.NODE_ENV,
9
10 // Sample rate for performance transactions
11 tracesSampleRate: 1.0,
12
13 // Enable Replay for user sessions
14 replaysSessionSampleRate: 0.1, // 10% of sessions
15 replaysOnErrorSampleRate: 1.0, // 100% of sessions with errors
16
17 // Integrations
18 integrations: [
19 new Sentry.Replay({
20 maskAllText: false,
21 blockAllMedia: false,
22 }),
23 ],
24
25 // Filtering errors
26 beforeSend(event) {
27 // Ignore certain types of errors
28 if (event.exception) {
29 const error = event.exception.values[0];
30 if (error.type === 'ChunkLoadError') {
31 return null; // Ignore chunk loading errors
32 }
33 }
34 return event;
35 },
36});1// sentry.server.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6 environment: process.env.NODE_ENV,
7 tracesSampleRate: 1.0,
8
9 // Additional options for the server
10 debug: false,
11
12 // Integrations specific to Node.js
13 integrations: [
14 new Sentry.Integrations.Http({ tracing: true }),
15 ],
16});1// sentry.edge.config.js
2import * as Sentry from '@sentry/nextjs';
3
4Sentry.init({
5 dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
6 environment: process.env.NODE_ENV,
7 tracesSampleRate: 1.0,
8 debug: false,
9});2. Configuring Next.js for Sentry
1// next.config.js
2const { withSentryConfig } = require('@sentry/nextjs');
3
4const nextConfig = {
5 // Your standard Next.js configuration
6};
7
8const sentryWebpackPluginOptions = {
9 // Additional configuration options for the Sentry webpack plugin
10 silent: true, // Silence Sentry logs during the build
11 org: process.env.SENTRY_ORG,
12 project: process.env.SENTRY_PROJECT,
13};
14
15module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions);3. Adding context to errors
1// lib/sentry.ts
2import * as Sentry from '@sentry/nextjs';
3
4// Function to add user context
5export function setUserContext(user: { id: string; email: string; name?: string }) {
6 Sentry.setUser({
7 id: user.id,
8 email: user.email,
9 username: user.name,
10 });
11}
12
13// Function to add tags
14export function addTags(tags: Record<string, string>) {
15 Sentry.setTags(tags);
16}
17
18// Function for manual error reporting
19export function captureException(error: Error, context?: Record<string, any>) {
20 Sentry.withScope((scope) => {
21 if (context) {
22 scope.setContext('additional_info', context);
23 }
24 Sentry.captureException(error);
25 });
26}
27
28// Function for performance tracking
29export function startTransaction(name: string, op: string) {
30 return Sentry.startTransaction({ name, op });
31}4. Using Sentry in components
1// components/ErrorBoundary.tsx
2'use client';
3
4import React from 'react';
5import * as Sentry from '@sentry/nextjs';
6
7interface ErrorBoundaryState {
8 hasError: boolean;
9 eventId?: string;
10}
11
12class ErrorBoundary extends React.Component<
13 React.PropsWithChildren<{}>,
14 ErrorBoundaryState
15> {
16 constructor(props: React.PropsWithChildren<{}>) {
17 super(props);
18 this.state = { hasError: false };
19 }
20
21 static getDerivedStateFromError(_: Error): ErrorBoundaryState {
22 return { hasError: true };
23 }
24
25 componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
26 const eventId = Sentry.captureException(error, {
27 contexts: {
28 react: {
29 componentStack: errorInfo.componentStack,
30 },
31 },
32 });
33
34 this.setState({ eventId });
35 }
36
37 render() {
38 if (this.state.hasError) {
39 return (
40 <div className="error-fallback">
41 <h2>Oops! Something went wrong</h2>
42 <p>An unexpected error occurred. Our team has been notified.</p>
43 <details style={{ whiteSpace: 'pre-wrap' }}>
44 Error ID: {this.state.eventId}
45 </details>
46 <button onClick={() => window.location.reload()}>
47 Refresh the page
48 </button>
49 </div>
50 );
51 }
52
53 return this.props.children;
54 }
55}
56
57export default ErrorBoundary;Monitoring Core Web Vitals
Core Web Vitals are key performance metrics that Google uses to evaluate the user experience:
- LCP (Largest Contentful Paint) - loading time of the largest element
- FID (First Input Delay) - response time for the first interaction
- CLS (Cumulative Layout Shift) - layout instability
1. Web Vitals implementation in Next.js
1// lib/web-vitals.ts
2import { getCLS, getFID, getFCP, getLCP, getTTFB } from 'web-vitals';
3import * as Sentry from '@sentry/nextjs';
4
5interface WebVitalMetric {
6 name: string;
7 value: number;
8 id: string;
9 delta: number;
10}
11
12// Function for reporting metrics
13function sendToAnalytics(metric: WebVitalMetric) {
14 // Send to Google Analytics
15 if (typeof window !== 'undefined' && window.gtag) {
16 window.gtag('event', metric.name, {
17 event_category: 'Web Vitals',
18 event_label: metric.id,
19 value: Math.round(metric.name === 'CLS' ? metric.value * 1000 : metric.value),
20 non_interaction: true,
21 });
22 }
23
24 // Send to Sentry
25 Sentry.addBreadcrumb({
26 category: 'web-vital',
27 message: `${metric.name}: ${metric.value}`,
28 level: 'info',
29 data: {
30 name: metric.name,
31 value: metric.value,
32 id: metric.id,
33 delta: metric.delta,
34 },
35 });
36
37 // In case of poor results, send as event to Sentry
38 const thresholds = {
39 LCP: 2500,
40 FID: 100,
41 CLS: 0.1,
42 };
43
44 if (metric.value > thresholds[metric.name as keyof typeof thresholds]) {
45 Sentry.captureMessage(`Poor ${metric.name}: ${metric.value}`, 'warning');
46 }
47}
48
49// Initialize Web Vitals measurements
50export function initWebVitals() {
51 getCLS(sendToAnalytics);
52 getFID(sendToAnalytics);
53 getFCP(sendToAnalytics);
54 getLCP(sendToAnalytics);
55 getTTFB(sendToAnalytics);
56}2. Component for monitoring Web Vitals
1// components/WebVitalsReporter.tsx
2'use client';
3
4import { useEffect } from 'react';
5import { useReportWebVitals } from 'next/web-vitals';
6
7export default function WebVitalsReporter() {
8 useReportWebVitals((metric) => {
9 // Send to various analytics systems
10 switch (metric.name) {
11 case 'FCP':
12 case 'LCP':
13 case 'CLS':
14 case 'FID':
15 case 'TTFB':
16 // Send to your own API
17 fetch('/api/analytics/web-vitals', {
18 method: 'POST',
19 headers: { 'Content-Type': 'application/json' },
20 body: JSON.stringify(metric),
21 }).catch(console.error);
22 break;
23 default:
24 break;
25 }
26 });
27
28 return null;
29}3. API endpoint for collecting metrics
1// app/api/analytics/web-vitals/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { prisma } from '@/lib/prisma'; // Let's assume you use Prisma
4
5export async function POST(request: NextRequest) {
6 try {
7 const metric = await request.json();
8
9 // Basic validation
10 if (!metric.name || !metric.value) {
11 return NextResponse.json({ error: 'Invalid metric data' }, { status: 400 });
12 }
13
14 // Save to the database
15 await prisma.webVitalMetric.create({
16 data: {
17 name: metric.name,
18 value: metric.value,
19 id: metric.id,
20 delta: metric.delta,
21 url: request.headers.get('referer') || '',
22 userAgent: request.headers.get('user-agent') || '',
23 timestamp: new Date(),
24 },
25 });
26
27 return NextResponse.json({ success: true });
28 } catch (error) {
29 console.error('Error saving web vital metric:', error);
30 return NextResponse.json({ error: 'Internal server error' }, { status: 500 });
31 }
32}Performance monitoring with New Relic
New Relic offers a comprehensive solution for performance monitoring of Next.js applications.
1. Configuring New Relic
1npm install newrelic1// newrelic.js (in the project root)
2'use strict';
3
4exports.config = {
5 app_name: ['Next.js App'],
6 license_key: process.env.NEW_RELIC_LICENSE_KEY,
7
8 // Logging configuration
9 logging: {
10 level: 'info',
11 },
12
13 // Enable distributed tracing
14 distributed_tracing: {
15 enabled: true,
16 },
17
18 // Browser monitoring
19 browser_monitoring: {
20 enable: true,
21 },
22
23 // Configuration specific to Next.js
24 allow_all_headers: true,
25 attributes: {
26 exclude: [
27 'request.headers.cookie',
28 'request.headers.authorization',
29 'request.headers.x-*',
30 ],
31 },
32};1// next.config.js
2if (process.env.NODE_ENV === 'production') {
3 require('./newrelic');
4}
5
6const nextConfig = {
7 // Your Next.js configuration
8};
9
10module.exports = nextConfig;2. Custom instrumentation in New Relic
1// lib/monitoring.ts
2import newrelic from 'newrelic';
3
4// Function to track custom metrics
5export function recordCustomMetric(name: string, value: number) {
6 if (process.env.NODE_ENV === 'production') {
7 newrelic.recordMetric(name, value);
8 }
9}
10
11// Function to track business events
12export function recordCustomEvent(eventType: string, attributes: Record<string, any>) {
13 if (process.env.NODE_ENV === 'production') {
14 newrelic.recordCustomEvent(eventType, attributes);
15 }
16}
17
18// Wrapper for API calls with automatic tracking
19export async function instrumentedApiCall<T>(
20 name: string,
21 apiCall: () => Promise<T>
22): Promise<T> {
23 const startTime = Date.now();
24
25 try {
26 const result = await apiCall();
27
28 // Save success metrics
29 recordCustomMetric(`Custom/API/${name}/Duration`, Date.now() - startTime);
30 recordCustomEvent('APICall', {
31 name,
32 status: 'success',
33 duration: Date.now() - startTime,
34 });
35
36 return result;
37 } catch (error) {
38 // Save error metrics
39 recordCustomMetric(`Custom/API/${name}/Error`, 1);
40 recordCustomEvent('APICall', {
41 name,
42 status: 'error',
43 duration: Date.now() - startTime,
44 error: error.message,
45 });
46
47 throw error;
48 }
49}Vercel Analytics - Next-generation monitoring in Quantum Metropolis
In Quantum Metropolis 2150, every user interaction is monitored by advanced analytics systems. Vercel Analytics is a native monitoring solution for Next.js applications, designed with user privacy and GDPR compliance in mind. Unlike Google Analytics, Vercel Analytics runs directly in the Vercel Edge Network, which ensures minimal latency and accurate measurements.
Vercel Analytics vs Google Analytics
Before you decide on an analytics tool, it's worth understanding the key differences:
Vercel Analytics:
- Integrated with Vercel Edge Network
- Automatic tracking of Core Web Vitals
- Privacy-first (no cookies, GDPR-compliant)
- Zero configuration for basic metrics
- Real-time data in the Vercel dashboard
- Paid plan for a larger number of page views
- Basic segmentation features
Google Analytics 4:
- Advanced user segmentation
- Funnel analysis and conversion tracking
- Integration with Google Ads and other Google tools
- Free plan for most use cases
- Requires cookie consent (GDPR compliance)
- Greater impact on page performance
- More complicated configuration
Real User Monitoring (RUM) with Vercel Analytics
Real User Monitoring is a technique for collecting data about real user experiences, not synthetic tests. Vercel Analytics automatically tracks all key performance metrics.
Installing Vercel Analytics
1npm install @vercel/analytics
2# or
3yarn add @vercel/analytics
4# or
5pnpm add @vercel/analyticsBasic configuration
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3
4export default function RootLayout({
5 children
6}: {
7 children: React.ReactNode
8}) {
9 return (
10 <html lang="pl">
11 <body>
12 {children}
13 <Analytics />
14 </body>
15 </html>
16 );
17}That's all! Vercel Analytics will automatically start tracking:
- Page views
- Unique visitors
- Top pages
- Top referrers
- Devices and browsers
- Geographic data
Core Web Vitals Tracking
Core Web Vitals are key web performance metrics that Google uses as ranking signals. Vercel Analytics automatically tracks all three metrics:
1. Largest Contentful Paint (LCP)
- Measures the loading time of the largest content element
- Target: < 2.5s
- Affects: First impression, perceived performance
2. First Input Delay (FID) / Interaction to Next Paint (INP)
- Measures responsiveness to user interactions
- FID target: < 100ms
- INP target: < 200ms
- Affects: Interactivity, user experience
3. Cumulative Layout Shift (CLS)
- Measures visual stability (unexpected layout shifts)
- Target: < 0.1
- Affects: Visual stability, user frustration
Access to Core Web Vitals in code
You can programmatically access Web Vitals metrics:
1// app/web-vitals.tsx
2'use client';
3
4import { useReportWebVitals } from 'next/web-vitals';
5
6export function WebVitals() {
7 useReportWebVitals((metric) => {
8 console.log(metric);
9
10 // You can send metrics to your own endpoint
11 if (metric.label === 'web-vital') {
12 fetch('/api/analytics/web-vitals', {
13 method: 'POST',
14 body: JSON.stringify(metric),
15 headers: {
16 'Content-Type': 'application/json'
17 }
18 });
19 }
20 });
21
22 return null;
23}1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { WebVitals } from './web-vitals';
4
5export default function RootLayout({ children }: { children: React.ReactNode }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <Analytics />
11 <WebVitals />
12 </body>
13 </html>
14 );
15}Custom Events - tracking custom interactions
Vercel Analytics allows tracking custom events for specific user actions:
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function NewsletterForm() {
6 const handleSubscribe = async (email: string) => {
7 try {
8 await subscribeToNewsletter(email);
9
10 // Track successful subscription
11 track('Newsletter Subscribe', {
12 email_domain: email.split('@')[1],
13 source: 'homepage'
14 });
15
16 } catch (error) {
17 // Track error
18 track('Newsletter Subscribe Error', {
19 error: error.message
20 });
21 }
22 };
23
24 return (
25 <form onSubmit={(e) => {
26 e.preventDefault();
27 const email = e.target.email.value;
28 handleSubscribe(email);
29 }}>
30 <input type="email" name="email" required />
31 <button type="submit">Subscribe</button>
32 </form>
33 );
34}Advanced custom events
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function ProductPage({ product }: { product: Product }) {
6 // Track product view
7 useEffect(() => {
8 track('Product View', {
9 product_id: product.id,
10 product_name: product.name,
11 product_category: product.category,
12 product_price: product.price
13 });
14 }, [product]);
15
16 // Track add to cart
17 const handleAddToCart = () => {
18 track('Add to Cart', {
19 product_id: product.id,
20 quantity: 1,
21 value: product.price
22 });
23
24 addToCart(product);
25 };
26
27 // Track checkout start
28 const handleCheckout = () => {
29 track('Begin Checkout', {
30 value: cartTotal,
31 items: cartItems.length
32 });
33
34 router.push('/checkout');
35 };
36
37 return (
38 <div>
39 <h1>{product.name}</h1>
40 <p>{product.price} PLN</p>
41 <button onClick={handleAddToCart}>Add to cart</button>
42 <button onClick={handleCheckout}>Buy now</button>
43 </div>
44 );
45}Vercel Speed Insights - real-time performance monitoring
Speed Insights is an Analytics extension that provides detailed information about application performance for real users.
Installation
1npm install @vercel/speed-insightsConfiguration
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { SpeedInsights } from '@vercel/speed-insights/next';
4
5export default function RootLayout({ children }: { children: React.ReactNode }) {
6 return (
7 <html lang="pl">
8 <body>
9 {children}
10 <Analytics />
11 <SpeedInsights />
12 </body>
13 </html>
14 );
15}What does Speed Insights track?
Performance Score
- Overall performance rating (0-100)
- Based on Core Web Vitals and other metrics
Field Data (RUM)
- Real data from users
- Segmented by device type, connection speed, geographic location
Lab Data
- Synthetic performance tests
- Controlled conditions for consistency
Opportunities
- Specific optimization suggestions
- Estimated time savings
Combination with other analytics tools
You can combine Vercel Analytics with other tools for a fuller picture:
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3import { SpeedInsights } from '@vercel/speed-insights/next';
4import { GoogleAnalytics } from '@next/third-parties/google';
5
6export default function RootLayout({ children }: { children: React.ReactNode }) {
7 return (
8 <html lang="pl">
9 <body>
10 {children}
11
12 {/* Vercel Analytics - for Core Web Vitals and basic metrics */}
13 <Analytics />
14 <SpeedInsights />
15
16 {/* Google Analytics - for advanced segmentation */}
17 <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
18 </body>
19 </html>
20 );
21}Best practices for Vercel Analytics
1. Use environment-specific tracking
1// app/layout.tsx
2import { Analytics } from '@vercel/analytics/react';
3
4export default function RootLayout({ children }: { children: React.ReactNode }) {
5 return (
6 <html lang="pl">
7 <body>
8 {children}
9
10 {/* Only in production */}
11 {process.env.NODE_ENV === 'production' && <Analytics />}
12 </body>
13 </html>
14 );
15}2. Group custom events logically
1// lib/analytics.ts
2import { track } from '@vercel/analytics';
3
4export const analytics = {
5 // User events
6 user: {
7 signup: (method: string) => track('User Signup', { method }),
8 login: (method: string) => track('User Login', { method }),
9 logout: () => track('User Logout')
10 },
11
12 // E-commerce events
13 ecommerce: {
14 viewProduct: (productId: string) =>
15 track('Product View', { product_id: productId }),
16 addToCart: (productId: string, value: number) =>
17 track('Add to Cart', { product_id: productId, value }),
18 purchase: (orderId: string, value: number) =>
19 track('Purchase', { order_id: orderId, value })
20 },
21
22 // Engagement events
23 engagement: {
24 shareContent: (contentType: string, method: string) =>
25 track('Share', { content_type: contentType, method }),
26 search: (query: string) =>
27 track('Search', { search_term: query }),
28 playVideo: (videoId: string) =>
29 track('Video Play', { video_id: videoId })
30 }
31};3. Monitor the conversion funnel
1'use client';
2
3import { track } from '@vercel/analytics';
4
5export default function CheckoutFlow() {
6 // Step 1: View cart
7 useEffect(() => {
8 track('Checkout Step 1 - Cart', {
9 items: cart.length,
10 value: cartTotal
11 });
12 }, []);
13
14 // Step 2: Shipping info
15 const handleShippingSubmit = () => {
16 track('Checkout Step 2 - Shipping', {
17 value: cartTotal
18 });
19 };
20
21 // Step 3: Payment
22 const handlePaymentSubmit = () => {
23 track('Checkout Step 3 - Payment', {
24 value: cartTotal,
25 payment_method: selectedPaymentMethod
26 });
27 };
28
29 // Step 4: Confirmation
30 const handleOrderComplete = (orderId: string) => {
31 track('Purchase Complete', {
32 order_id: orderId,
33 value: cartTotal,
34 items: cart.length
35 });
36 };
37
38 return <CheckoutSteps />;
39}Dashboard and reports
The Vercel Analytics Dashboard offers:
Overview
- Total page views
- Unique visitors
- Top pages
- Top referrers
Audiences
- Device breakdown (mobile, desktop, tablet)
- Browser usage
- Operating systems
- Geographic distribution
Real-time
- Live visitor count
- Current popular pages
- Recent events
Web Vitals
- LCP, FID/INP, CLS scores
- Performance over time
- Device-specific metrics
Custom Events
- Event counts
- Event properties
- Conversion tracking
Limitations and pricing
Free tier:
- 2,500 events/month
- Basic analytics
- 1 member
Pro tier ($10/member/month):
- 100,000 events/month
- Advanced filtering
- Unlimited members
- Data export
Enterprise:
- Custom event limits
- Dedicated support
- SLA guarantees
Integration with Vercel Edge Config
You can dynamically control feature flags based on analytics:
1// lib/feature-flags.ts
2import { get } from '@vercel/edge-config';
3import { track } from '@vercel/analytics';
4
5export async function checkFeatureFlag(flagName: string): Promise<boolean> {
6 const isEnabled = await get(flagName);
7
8 // Track feature flag usage
9 if (isEnabled) {
10 track('Feature Flag Enabled', { flag: flagName });
11 }
12
13 return isEnabled || false;
14}Summary Vercel Analytics
In Quantum Metropolis 2150, where every millisecond matters, Vercel Analytics provides quantum precision in Next.js application monitoring:
Key advantages:
- Zero-config setup for basic metrics
- Automatic tracking of Core Web Vitals
- Privacy-first approach (GDPR compliant)
- Real-time data in the Vercel Dashboard
- Integration with Vercel Edge Network
- Custom events for business metrics
When to use Vercel Analytics:
- Applications hosted on Vercel
- You need privacy-first analytics
- You want zero-config Web Vitals monitoring
- Basic analytics is enough for your case
- Budget allows for a paid plan for larger traffic
When to consider alternatives:
- You need advanced user segmentation
- You require funnel analysis and attribution modeling
- You have very high traffic (> 1M page views/month)
- You need integration with ad platforms
- You prefer a self-hosted solution
Vercel Analytics is the ideal solution for most Next.js applications, offering an excellent balance between simplicity, performance, and functionality. Combined with Speed Insights, it creates a complete real-time application performance monitoring system!
Custom monitoring with Prometheus and Grafana
For more advanced monitoring, you can configure your own stack with Prometheus and Grafana.
1. Configuring Prometheus metrics
1// lib/metrics.ts
2import client from 'prom-client';
3
4// Metrics registry
5const register = new client.Registry();
6
7// HTTP metrics
8const httpRequestDuration = new client.Histogram({
9 name: 'http_request_duration_seconds',
10 help: 'Duration of HTTP requests in seconds',
11 labelNames: ['method', 'route', 'status_code'],
12 buckets: [0.1, 0.3, 0.5, 0.7, 1, 3, 5, 7, 10],
13});
14
15const httpRequestTotal = new client.Counter({
16 name: 'http_requests_total',
17 help: 'Total number of HTTP requests',
18 labelNames: ['method', 'route', 'status_code'],
19});
20
21// Business metrics
22const userRegistrations = new client.Counter({
23 name: 'user_registrations_total',
24 help: 'Total number of user registrations',
25});
26
27const ordersTotal = new client.Counter({
28 name: 'orders_total',
29 help: 'Total number of orders',
30 labelNames: ['status'],
31});
32
33// Register metrics
34register.registerMetric(httpRequestDuration);
35register.registerMetric(httpRequestTotal);
36register.registerMetric(userRegistrations);
37register.registerMetric(ordersTotal);
38
39// Add default Node.js metrics
40client.collectDefaultMetrics({ register });
41
42export {
43 register,
44 httpRequestDuration,
45 httpRequestTotal,
46 userRegistrations,
47 orderTotal
48};2. Middleware for HTTP tracking
1// middleware.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { httpRequestDuration, httpRequestTotal } from '@/lib/metrics';
4
5export function middleware(request: NextRequest) {
6 const start = Date.now();
7
8 // Continue processing the request
9 const response = NextResponse.next();
10
11 // Save metrics after processing the request
12 response.headers.set('x-response-time', (Date.now() - start).toString());
13
14 // Async metric saving (does not block the response)
15 setImmediate(() => {
16 const duration = (Date.now() - start) / 1000;
17 const labels = {
18 method: request.method,
19 route: request.nextUrl.pathname,
20 status_code: response.status.toString(),
21 };
22
23 httpRequestDuration.observe(labels, duration);
24 httpRequestTotal.inc(labels);
25 });
26
27 return response;
28}
29
30export const config = {
31 matcher: [
32 '/((?!api|_next/static|_next/image|favicon.ico).*)',
33 ],
34};3. Endpoint for Prometheus metrics
1// app/api/metrics/route.ts
2import { NextRequest, NextResponse } from 'next/server';
3import { register } from '@/lib/metrics';
4
5export async function GET(request: NextRequest) {
6 const metrics = await register.metrics();
7
8 return new NextResponse(metrics, {
9 headers: {
10 'Content-Type': register.contentType,
11 },
12 });
13}Alerting and notifications
An important aspect of monitoring is automatic notification of problems.
1. Integration with Slack
1// lib/alerts.ts
2import { IncomingWebhook } from '@slack/webhook';
3
4const webhook = new IncomingWebhook(process.env.SLACK_WEBHOOK_URL);
5
6export async function sendSlackAlert(message: string, severity: 'info' | 'warning' | 'error') {
7 const colors = {
8 info: '#36a64f',
9 warning: '#ffcc00',
10 error: '#ff0000',
11 };
12
13 try {
14 await webhook.send({
15 attachments: [
16 {
17 color: colors[severity],
18 fields: [
19 {
20 title: 'Alert',
21 value: message,
22 short: false,
23 },
24 {
25 title: 'Time',
26 value: new Date().toISOString(),
27 short: true,
28 },
29 {
30 title: 'Environment',
31 value: process.env.NODE_ENV,
32 short: true,
33 },
34 ],
35 },
36 ],
37 });
38 } catch (error) {
39 console.error('Failed to send Slack alert:', error);
40 }
41}
42
43// Function for checking thresholds and sending alerts
44export function checkPerformanceThresholds(metrics: {
45 lcp: number;
46 fid: number;
47 cls: number;
48}) {
49 if (metrics.lcp > 2500) {
50 sendSlackAlert(`High LCP detected: ${metrics.lcp}ms`, 'warning');
51 }
52
53 if (metrics.fid > 100) {
54 sendSlackAlert(`High FID detected: ${metrics.fid}ms`, 'warning');
55 }
56
57 if (metrics.cls > 0.1) {
58 sendSlackAlert(`High CLS detected: ${metrics.cls}`, 'warning');
59 }
60}2. Monitoring uptime
1// lib/uptime-monitor.ts
2import { sendSlackAlert } from './alerts';
3
4interface HealthCheckResult {
5 service: string;
6 status: 'healthy' | 'unhealthy';
7 responseTime: number;
8 timestamp: Date;
9}
10
11export async function checkServiceHealth(url: string, service: string): Promise<HealthCheckResult> {
12 const start = Date.now();
13
14 try {
15 const response = await fetch(url, {
16 method: 'GET',
17 timeout: 10000, // 10 second timeout
18 });
19
20 const responseTime = Date.now() - start;
21
22 if (response.ok) {
23 return {
24 service,
25 status: 'healthy',
26 responseTime,
27 timestamp: new Date(),
28 };
29 } else {
30 await sendSlackAlert(`Service ${service} returned ${response.status}`, 'error');
31 return {
32 service,
33 status: 'unhealthy',
34 responseTime,
35 timestamp: new Date(),
36 };
37 }
38 } catch (error) {
39 await sendSlackAlert(`Service ${service} is unreachable: ${error.message}`, 'error');
40 return {
41 service,
42 status: 'unhealthy',
43 responseTime: Date.now() - start,
44 timestamp: new Date(),
45 };
46 }
47}
48
49// Function for running health checks
50export async function runHealthChecks() {
51 const services = [
52 { name: 'Main App', url: 'https://your-app.com/api/health' },
53 { name: 'Database', url: 'https://your-app.com/api/health/db' },
54 { name: 'External API', url: 'https://api.external-service.com/health' },
55 ];
56
57 const results = await Promise.allSettled(
58 services.map(service => checkServiceHealth(service.url, service.name))
59 );
60
61 results.forEach((result, index) => {
62 if (result.status === 'fulfilled') {
63 console.log(`Health check for ${services[index].name}:`, result.value);
64 } else {
65 console.error(`Health check failed for ${services[index].name}:`, result.reason);
66 }
67 });
68}Dashboard and data visualization
1. A simple dashboard in Next.js
1// app/admin/monitoring/page.tsx
2import { getWebVitalMetrics, getErrorMetrics } from '@/lib/analytics';
3import { LineChart, BarChart } from '@/components/Charts';
4
5export default async function MonitoringDashboard() {
6 const webVitals = await getWebVitalMetrics();
7 const errors = await getErrorMetrics();
8
9 return (
10 <div className="p-6">
11 <h1 className="text-3xl font-bold mb-8">Monitoring Dashboard</h1>
12
13 <div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
14 {/* Core Web Vitals */}
15 <div className="bg-white p-6 rounded-lg shadow">
16 <h2 className="text-xl font-semibold mb-4">Core Web Vitals</h2>
17 <LineChart
18 data={webVitals}
19 categories={['lcp', 'fid', 'cls']}
20 xKey="timestamp"
21 />
22 </div>
23
24 {/* Error Rate */}
25 <div className="bg-white p-6 rounded-lg shadow">
26 <h2 className="text-xl font-semibold mb-4">Error Rate</h2>
27 <BarChart
28 data={errors}
29 category="count"
30 xKey="hour"
31 />
32 </div>
33 </div>
34
35 {/* Real-time metrics */}
36 <div className="grid grid-cols-1 md:grid-cols-4 gap-4">
37 <MetricCard title="Active Users" value="1,234" change="+5.2%" />
38 <MetricCard title="Response Time" value="156ms" change="-2.1%" />
39 <MetricCard title="Error Rate" value="0.01%" change="-50%" />
40 <MetricCard title="Uptime" value="99.9%" change="+0.1%" />
41 </div>
42 </div>
43 );
44}
45
46function MetricCard({ title, value, change }: {
47 title: string;
48 value: string;
49 change: string;
50}) {
51 const isPositive = change.startsWith('+');
52 const isNegative = change.startsWith('-');
53
54 return (
55 <div className="bg-white p-4 rounded-lg shadow">
56 <h3 className="text-sm font-medium text-gray-500">{title}</h3>
57 <div className="mt-2 flex items-baseline">
58 <p className="text-2xl font-semibold text-gray-900">{value}</p>
59 <p className={`ml-2 text-sm font-medium ${
60 isPositive ? 'text-green-600' : isNegative ? 'text-red-600' : 'text-gray-500'
61 }`}>
62 {change}
63 </p>
64 </div>
65 </div>
66 );
67}Monitoring best practices
1. Sampling strategies
1// lib/sampling.ts
2
3// Function for determining whether an event should be tracked
4export function shouldSample(sampleRate: number): boolean {
5 return Math.random() < sampleRate;
6}
7
8// Adaptive sampling - more samples for errors
9export function adaptiveSample(isError: boolean): boolean {
10 return isError ? shouldSample(1.0) : shouldSample(0.1);
11}
12
13// Sampling based on user tier
14export function userTierSample(userTier: 'free' | 'premium' | 'enterprise'): boolean {
15 const rates = {
16 free: 0.05, // 5% for free users
17 premium: 0.25, // 25% for premium
18 enterprise: 1.0, // 100% for enterprise
19 };
20
21 return shouldSample(rates[userTier]);
22}2. Monitoring performance optimization
1// lib/performance-monitoring.ts
2
3// Buffer for metrics - collect and send in batches
4class MetricsBuffer {
5 private buffer: any[] = [];
6 private maxSize = 100;
7 private flushInterval = 30000; // 30 seconds
8
9 constructor() {
10 // Automatic buffer flushing
11 setInterval(() => this.flush(), this.flushInterval);
12 }
13
14 add(metric: any) {
15 this.buffer.push(metric);
16
17 if (this.buffer.length >= this.maxSize) {
18 this.flush();
19 }
20 }
21
22 private async flush() {
23 if (this.buffer.length === 0) return;
24
25 const metricsToSend = [...this.buffer];
26 this.buffer = [];
27
28 try {
29 await fetch('/api/metrics/batch', {
30 method: 'POST',
31 headers: { 'Content-Type': 'application/json' },
32 body: JSON.stringify({ metrics: metricsToSend }),
33 });
34 } catch (error) {
35 console.error('Failed to send metrics batch:', error);
36 // Return metrics to the buffer in case of error
37 this.buffer.unshift(...metricsToSend);
38 }
39 }
40}
41
42export const metricsBuffer = new MetricsBuffer();Summary
Performance monitoring and error analytics are key elements of any production Next.js application. A comprehensive monitoring approach includes:
- Error tracking - automatic detection and reporting of problems
- Performance monitoring - Core Web Vitals and other key metrics
- Alerting - immediate notifications about critical problems
- Dashboards - data visualization for decision-making
- User analytics - understanding how users use the application
When choosing monitoring tools, consider:
- Sentry - for comprehensive error tracking
- Vercel Analytics - for simple monitoring on Vercel
- New Relic/DataDog - for advanced performance monitoring
- Custom solutions - for specialized needs
Remember the balance between monitoring depth and impact on application performance. Use sampling, buffering, and optimize queries so that monitoring does not negatively affect user experience.
Code for this lesson: App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Demo: Performance monitoring and error analytics
4interface ErrorEntry {
5 id: string;
6 type: 'error' | 'warning' | 'info';
7 message: string;
8 stack: string;
9 count: number;
10 lastSeen: string;
11 status: 'new' | 'acknowledged' | 'resolved';
12}
13
14interface PerformanceMetric {
15 name: string;
16 value: number;
17 unit: string;
18 status: 'good' | 'needs-improvement' | 'poor';
19 threshold: { good: number; poor: number };
20}
21
22function MonitoringDemo() {
23 const [errors, setErrors] = useState<ErrorEntry[]>([
24 { id: '1', type: 'error', message: 'TypeError: Cannot read property of undefined', stack: 'at UserProfile (/app/components/UserProfile.tsx:42)', count: 23, lastSeen: '2 min ago', status: 'new' },
25 { id: '2', type: 'error', message: 'NetworkError: Failed to fetch /api/data', stack: 'at fetchData (/app/lib/api.ts:15)', count: 8, lastSeen: '15 min ago', status: 'acknowledged' },
26 { id: '3', type: 'warning', message: 'Large bundle size: chunk-vendors.js (450KB)', stack: 'Build warning', count: 1, lastSeen: '1 hour ago', status: 'new' },
27 { id: '4', type: 'error', message: 'Hydration mismatch in <Header />', stack: 'at Header (/app/components/Header.tsx:18)', count: 156, lastSeen: '30 sec ago', status: 'new' },
28 ]);
29
30 const [metrics, setMetrics] = useState<PerformanceMetric[]>([
31 { name: 'FCP', value: 1.2, unit: 's', status: 'good', threshold: { good: 1.8, poor: 3.0 } },
32 { name: 'LCP', value: 2.4, unit: 's', status: 'needs-improvement', threshold: { good: 2.5, poor: 4.0 } },
33 { name: 'CLS', value: 0.05, unit: '', status: 'good', threshold: { good: 0.1, poor: 0.25 } },
34 { name: 'FID', value: 45, unit: 'ms', status: 'good', threshold: { good: 100, poor: 300 } },
35 { name: 'TTFB', value: 320, unit: 'ms', status: 'needs-improvement', threshold: { good: 200, poor: 600 } },
36 { name: 'INP', value: 180, unit: 'ms', status: 'needs-improvement', threshold: { good: 200, poor: 500 } },
37 ]);
38
39 const [activeTab, setActiveTab] = useState<'errors' | 'performance' | 'setup'>('errors');
40 const [selectedTool, setSelectedTool] = useState<'sentry' | 'vercel' | 'custom'>('sentry');
41
42 const statusColors: Record<string, string> = { good: '#10b981', 'needs-improvement': '#f59e0b', poor: '#ef4444' };
43 const errorTypeColors: Record<string, string> = { error: '#ef4444', warning: '#f59e0b', info: '#3b82f6' };
44 const errorStatusColors: Record<string, string> = { new: '#ef4444', acknowledged: '#f59e0b', resolved: '#10b981' };
45
46 const updateErrorStatus = (id: string, status: ErrorEntry['status']) => {
47 setErrors(prev => prev.map(e => e.id === id ? { ...e, status } : e));
48 };
49
50 const triggerError = () => {
51 const newError: ErrorEntry = {
52 id: String(Date.now()),
53 type: 'error',
54 message: ['RangeError: Maximum call stack', 'SyntaxError: Unexpected token', 'ReferenceError: x is not defined'][Math.floor(Math.random() * 3)],
55 stack: 'at Component (/app/page.tsx:' + Math.floor(Math.random() * 100) + ')',
56 count: 1, lastSeen: 'just now', status: 'new',
57 };
58 setErrors(prev => [newError, ...prev]);
59 };
60
61 const setupCode: Record<string, string> = {
62 sentry: "// sentry.client.config.ts\nimport * as Sentry from '@sentry/nextjs';\n\nSentry.init({\n dsn: process.env.SENTRY_DSN,\n tracesSampleRate: 0.1,\n replaysSessionSampleRate: 0.1,\n integrations: [\n Sentry.replayIntegration(),\n Sentry.browserTracingIntegration(),\n ],\n});",
63 vercel: "// next.config.js\nmodule.exports = {\n experimental: {\n instrumentationHook: true,\n },\n};\n\n// instrumentation.ts\nexport async function register() {\n if (process.env.NEXT_RUNTIME === 'nodejs') {\n // Server-side monitoring\n }\n}",
64 custom: "// lib/monitoring.ts\nexport function reportError(error: Error, context?: object) {\n fetch('/api/errors', {\n method: 'POST',\n body: JSON.stringify({\n message: error.message,\n stack: error.stack,\n context,\n timestamp: Date.now(),\n }),\n });\n}",
65 };
66
67 return (
68 <div style={{ background: '#0f0f23', minHeight: '100vh', padding: '20px', color: '#e0e0e0', fontFamily: 'system-ui' }}>
69 <h1 style={{ color: '#64ffda', marginBottom: '8px' }}>Monitoring & Error Tracking</h1>
70 <p style={{ color: '#8892b0', marginBottom: '24px' }}>Performance monitoring and error tracking in Next.js</p>
71
72 <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
73 {[
74 { id: 'errors' as const, label: 'Errors (' + errors.filter(e => e.status === 'new').length + ')' },
75 { id: 'performance' as const, label: 'Performance' },
76 { id: 'setup' as const, label: 'Setup' },
77 ].map(tab => (
78 <button key={tab.id} onClick={() => setActiveTab(tab.id)} style={{
79 padding: '10px 20px', borderRadius: '8px', border: 'none', cursor: 'pointer',
80 background: activeTab === tab.id ? '#64ffda' : '#1a1a2e', color: activeTab === tab.id ? '#0f0f23' : '#8892b0', fontWeight: 'bold',
81 }}>{tab.label}</button>
82 ))}
83 </div>
84
85 {activeTab === 'errors' && (
86 <div>
87 <button onClick={triggerError} style={{ padding: '8px 16px', borderRadius: '6px', background: '#ef4444', color: '#fff', border: 'none', cursor: 'pointer', marginBottom: '16px' }}>
88 Simulate error
89 </button>
90 <div style={{ display: 'grid', gap: '8px' }}>
91 {errors.map(err => (
92 <div key={err.id} style={{ padding: '14px', borderRadius: '8px', background: '#1a1a2e', border: '1px solid #333' }}>
93 <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '8px' }}>
94 <div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
95 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '12px', background: errorTypeColors[err.type] + '30', color: errorTypeColors[err.type] }}>{err.type}</span>
96 <span style={{ fontSize: '11px', padding: '2px 8px', borderRadius: '12px', background: errorStatusColors[err.status] + '30', color: errorStatusColors[err.status] }}>{err.status}</span>
97 <span style={{ fontSize: '12px', color: '#8892b0' }}>x{err.count} | {err.lastSeen}</span>
98 </div>
99 <select value={err.status} onChange={e => updateErrorStatus(err.id, e.target.value as any)} style={{ padding: '4px 8px', borderRadius: '4px', background: '#0f0f23', color: '#fff', border: '1px solid #333', fontSize: '11px' }}>
100 <option value="new">New</option>
101 <option value="acknowledged">Acknowledged</option>
102 <option value="resolved">Resolved</option>
103 </select>
104 </div>
105 <div style={{ color: errorTypeColors[err.type], fontWeight: 'bold', fontSize: '14px' }}>{err.message}</div>
106 <div style={{ fontSize: '12px', color: '#555', marginTop: '4px' }}>{err.stack}</div>
107 </div>
108 ))}
109 </div>
110 </div>
111 )}
112
113 {activeTab === 'performance' && (
114 <div style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '16px' }}>
115 {metrics.map(m => (
116 <div key={m.name} style={{ background: '#1a1a2e', padding: '20px', borderRadius: '12px', border: '1px solid #333', textAlign: 'center' }}>
117 <div style={{ fontSize: '32px', fontWeight: 'bold', color: statusColors[m.status] }}>{m.value}{m.unit}</div>
118 <div style={{ fontSize: '16px', color: '#64ffda', marginTop: '4px' }}>{m.name}</div>
119 <div style={{ fontSize: '12px', color: statusColors[m.status], marginTop: '4px' }}>{m.status.replace('-', ' ')}</div>
120 <div style={{ marginTop: '8px', height: '4px', background: '#333', borderRadius: '2px', overflow: 'hidden' }}>
121 <div style={{ width: Math.min(100, (m.value / m.threshold.poor) * 100) + '%', height: '100%', background: statusColors[m.status] }} />
122 </div>
123 <div style={{ fontSize: '10px', color: '#555', marginTop: '4px' }}>Good: <{m.threshold.good}{m.unit} | Poor: >{m.threshold.poor}{m.unit}</div>
124 </div>
125 ))}
126 </div>
127 )}
128
129 {activeTab === 'setup' && (
130 <div>
131 <div style={{ display: 'flex', gap: '8px', marginBottom: '16px' }}>
132 {[
133 { id: 'sentry' as const, label: 'Sentry' },
134 { id: 'vercel' as const, label: 'Vercel Analytics' },
135 { id: 'custom' as const, label: 'Custom' },
136 ].map(tool => (
137 <button key={tool.id} onClick={() => setSelectedTool(tool.id)} style={{
138 padding: '8px 16px', borderRadius: '8px', border: 'none', cursor: 'pointer',
139 background: selectedTool === tool.id ? '#7c3aed' : '#1a1a2e', color: '#fff',
140 }}>{tool.label}</button>
141 ))}
142 </div>
143 <pre style={{ background: '#1a1a2e', padding: '16px', borderRadius: '8px', fontSize: '12px', overflow: 'auto', border: '1px solid #333', color: '#e0e0e0', whiteSpace: 'pre-wrap' }}>
144 {setupCode[selectedTool]}
145 </pre>
146 </div>
147 )}
148 </div>
149 );
150}
151
152export default MonitoringDemo;Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. Which Core Web Vitals metrics does Google use to evaluate page performance?
Hands-on tasks in the game
- Code editor
Create an application monitoring system: performance metrics, error tracking, uptime monitoring using Web Vitals
- Vertical ordering
Arrange the elements in the correct order: Choose tool β Add instrumentation β Set up dashboards
- Vertical ordering
Arrange the performance budget configuration steps in the correct order: