Cours Next.js · Module 8: Déploiement et production

Monitoring des performances et analyse des erreurs

23 min de lecture
Dans cette leçon12

Dans les applications Next.js en production, le monitoring des performances et le suivi des erreurs sont essentiels pour maintenir une haute qualité des services livrés. Dans ce module, nous allons découvrir des outils et des techniques qui nous permettront de monitorer efficacement l'application, de détecter les problèmes avant les utilisateurs et d'optimiser les performances en continu.

L'importance du monitoring dans les applications de production

Le monitoring est le processus d'observation continue d'une application dans le but de :

  1. Détecter tôt les problèmes - avant qu'ils n'affectent les utilisateurs
  2. Suivre les performances - temps de chargement, Core Web Vitals, débit
  3. Analyser le comportement des utilisateurs - comment les utilisateurs utilisent l'application
  4. Identifier les goulots d'étranglement - les endroits qui nécessitent une optimisation
  5. Garantir le SLA - respecter les niveaux de service convenus

Types de monitoring

1. Performance Monitoring

  • Core Web Vitals (LCP, FID, CLS)
  • Temps de chargement des pages
  • Temps de réponse de l'API
  • Consommation des ressources (CPU, mémoire)

2. Error Monitoring

  • Erreurs JavaScript
  • Erreurs d'API
  • Erreurs réseau
  • Stack traces

3. Monitoring de l'infrastructure

  • Disponibilité du serveur
  • Consommation des ressources
  • Métriques réseau

4. User Experience Monitoring (UX)

  • Real User Monitoring (RUM)
  • Enregistrements de sessions
  • Heat maps
  • Parcours utilisateurs

Intégration avec Sentry pour le monitoring des erreurs

Sentry est l'un des outils les plus populaires pour le monitoring des erreurs dans les applications JavaScript et Next.js.

1. Installation et configuration de Sentry

1npm install @sentry/nextjs
2# or
3yarn add @sentry/nextjs

Crée les fichiers de configuration :

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% des sessions
15  replaysOnErrorSampleRate: 1.0,  // 100% of sessions with errors
16
17  // Intégrations
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  // Options supplémentaires pour le serveur
10  debug: false,
11
12  // Intégrations spécifiques à 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. Configuration de Next.js pour Sentry

1// next.config.js
2const { withSentryConfig } = require('@sentry/nextjs');
3
4const nextConfig = {
5  // Ta configuration Next.js standard
6};
7
8const sentryWebpackPluginOptions = {
9  // Options de configuration supplémentaires pour le plugin webpack de Sentry
10  silent: true, // Fait taire les logs Sentry pendant le build
11  org: process.env.SENTRY_ORG,
12  project: process.env.SENTRY_PROJECT,
13};
14
15module.exports = withSentryConfig(nextConfig, sentryWebpackPluginOptions);

3. Ajouter du contexte aux erreurs

1// lib/sentry.ts
2import * as Sentry from '@sentry/nextjs';
3
4// Fonction pour ajouter le contexte utilisateur
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. Utilisation de Sentry dans les composants

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 des Core Web Vitals

Les Core Web Vitals sont des métriques de performance clés que Google utilise pour évaluer l'expérience utilisateur :

  • LCP (Largest Contentful Paint) - temps de chargement du plus grand élément
  • FID (First Input Delay) - temps de réponse à la première interaction
  • CLS (Cumulative Layout Shift) - instabilité de la mise en page

1. Implémentation de Web Vitals dans 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// Fonction pour reporter les métriques
13function sendToAnalytics(metric: WebVitalMetric) {
14  // Envoie vers 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  // Envoie vers 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  // En cas de mauvais résultats, envoie comme événement vers 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// Initialise les mesures Web Vitals
50export function initWebVitals() {
51  getCLS(sendToAnalytics);
52  getFID(sendToAnalytics);
53  getFCP(sendToAnalytics);
54  getLCP(sendToAnalytics);
55  getTTFB(sendToAnalytics);
56}

2. Composant pour le monitoring des 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. Endpoint API pour collecter les métriques

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    // Validation de base
10    if (!metric.name || !metric.value) {
11      return NextResponse.json({ error: 'Invalid metric data' }, { status: 400 });
12    }
13
14    // Enregistre dans la base de données
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}

Monitoring des performances avec New Relic

New Relic propose une solution complète pour le monitoring des performances des applications Next.js.

1. Configuration de New Relic

1npm install newrelic
1// newrelic.js (à la racine du projet)
2'use strict';
3
4exports.config = {
5  app_name: ['Next.js App'],
6  license_key: process.env.NEW_RELIC_LICENSE_KEY,
7
8  // Configuration du logging
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 spécifique à 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  // Ta configuration Next.js
8};
9
10module.exports = nextConfig;

2. Instrumentation personnalisée dans 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    // Enregistre les métriques de succès
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 - le monitoring nouvelle génération dans la Métropole Quantum

Dans la Métropole Quantum 2150, chaque interaction utilisateur est monitorée par des systèmes d'analytics avancés. Vercel Analytics est une solution de monitoring native pour les applications Next.js, conçue en pensant à la vie privée des utilisateurs et à la conformité RGPD. Contrairement à Google Analytics, Vercel Analytics tourne directement dans le Vercel Edge Network, ce qui garantit une latence minimale et des mesures précises.

Vercel Analytics vs Google Analytics

Avant de choisir un outil d'analytics, il vaut la peine de comprendre les différences clés :

Vercel Analytics :

  • Intégré au Vercel Edge Network
  • Suivi automatique des Core Web Vitals
  • Privacy-first (pas de cookies, conforme RGPD)
  • Zéro configuration pour les métriques de base
  • Données en temps réel dans le dashboard Vercel
  • Plan payant pour un plus grand nombre de pages vues
  • Fonctions de segmentation basiques

Google Analytics 4 :

  • Segmentation avancée des utilisateurs
  • Analyse d'entonnoirs et suivi des conversions
  • Intégration avec Google Ads et les autres outils Google
  • Plan gratuit pour la plupart des cas d'usage
  • Nécessite le consentement aux cookies (conformité RGPD)
  • Impact plus important sur les performances de la page
  • Configuration plus compliquée

Real User Monitoring (RUM) avec Vercel Analytics

Le Real User Monitoring est une technique de collecte de données sur les expériences réelles des utilisateurs, et non sur des tests synthétiques. Vercel Analytics suit automatiquement toutes les métriques de performance clés.

Installation de Vercel Analytics

1npm install @vercel/analytics
2# or
3yarn add @vercel/analytics
4# or
5pnpm add @vercel/analytics

Configuration de base

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}

C'est tout ! Vercel Analytics va automatiquement commencer à suivre :

  • Les pages vues
  • Les visiteurs uniques
  • Les pages les plus consultées
  • Les principaux référents
  • Les appareils et navigateurs
  • Les données géographiques

Core Web Vitals Tracking

Les Core Web Vitals sont des métriques de performance web clés que Google utilise comme signaux de classement. Vercel Analytics suit automatiquement les trois métriques :

1. Largest Contentful Paint (LCP)

  • Mesure le temps de chargement du plus grand élément de contenu
  • Objectif : < 2.5s
  • Affecte : la première impression, la performance perçue

2. First Input Delay (FID) / Interaction to Next Paint (INP)

  • Mesure la réactivité aux interactions de l'utilisateur
  • Objectif FID : < 100ms
  • Objectif INP : < 200ms
  • Affecte : l'interactivité, l'expérience utilisateur

3. Cumulative Layout Shift (CLS)

  • Mesure la stabilité visuelle (décalages de mise en page inattendus)
  • Objectif : < 0.1
  • Affecte : la stabilité visuelle, la frustration de l'utilisateur

Accéder aux Core Web Vitals dans le code

Tu peux accéder par programmation aux métriques Web Vitals :

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 - suivi d'interactions personnalisées

Vercel Analytics permet de suivre des événements personnalisés pour des actions utilisateur spécifiques :

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">Subskrybuj</button>
32    </form>
33  );
34}

Custom events avancés

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}>Ajouter au panier</button>
42      <button onClick={handleCheckout}>Kup teraz</button>
43    </div>
44  );
45}

Vercel Speed Insights - monitoring des performances en temps réel

Speed Insights est une extension d'Analytics qui fournit des informations détaillées sur les performances de l'application pour les utilisateurs réels.

Installation

1npm install @vercel/speed-insights

Configuration

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}

Que suit Speed Insights ?

  1. Performance Score

    • Note globale de performance (0-100)
    • Basée sur les Core Web Vitals et d'autres métriques
  2. Field Data (RUM)

    • Données réelles des utilisateurs
    • Segmentées par type d'appareil, vitesse de connexion, localisation géographique
  3. Lab Data

    • Tests de performance synthétiques
    • Conditions contrôlées pour la cohérence
  4. Opportunities

    • Suggestions d'optimisation concrètes
    • Gains de temps estimés

Combinaison avec d'autres outils d'analytics

Tu peux combiner Vercel Analytics avec d'autres outils pour une image plus complète :

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 - pour les Core Web Vitals et les métriques de base */}
13        <Analytics />
14        <SpeedInsights />
15
16        {/* Google Analytics - pour la segmentation avancée */}
17        <GoogleAnalytics gaId={process.env.NEXT_PUBLIC_GA_ID} />
18      </body>
19    </html>
20  );
21}

Bonnes pratiques pour Vercel Analytics

1. Utilise un tracking spécifique à l'environnement

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        {/* Uniquement en production */}
11        {process.env.NODE_ENV === 'production' && <Analytics />}
12      </body>
13    </html>
14  );
15}

2. Groupe les custom events logiquement

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. Monitore le tunnel de conversion

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 et rapports

Le Vercel Analytics Dashboard offre :

  1. Overview

    • Total des pages vues
    • Visiteurs uniques
    • Pages les plus consultées
    • Principaux référents
  2. Audiences

    • Répartition par appareil (mobile, desktop, tablette)
    • Utilisation des navigateurs
    • Systèmes d'exploitation
    • Distribution géographique
  3. Real-time

    • Nombre de visiteurs en direct
    • Pages populaires actuelles
    • Événements récents
  4. Web Vitals

    • Scores LCP, FID/INP, CLS
    • Performances dans le temps
    • Métriques spécifiques à l'appareil
  5. Custom Events

    • Nombre d'événements
    • Propriétés des événements
    • Suivi des conversions

Limitations et pricing

Free tier :

  • 2 500 événements/mois
  • Analytics de base
  • 1 membre

Pro tier (10 $/membre/mois) :

  • 100 000 événements/mois
  • Filtrage avancé
  • Membres illimités
  • Export des données

Enterprise :

  • Limites d'événements personnalisées
  • Support dédié
  • Garanties SLA

Intégration avec Vercel Edge Config

Tu peux contrôler dynamiquement les feature flags à partir des 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}

Résumé Vercel Analytics

Dans la Métropole Quantum 2150, où chaque milliseconde compte, Vercel Analytics fournit une précision quantique dans le monitoring des applications Next.js :

Avantages clés :

  • Configuration zero-config pour les métriques de base
  • Suivi automatique des Core Web Vitals
  • Approche privacy-first (conforme RGPD)
  • Données en temps réel dans le Vercel Dashboard
  • Intégration avec le Vercel Edge Network
  • Custom events pour les métriques métier

Quand utiliser Vercel Analytics :

  • Applications hébergées sur Vercel
  • Tu as besoin d'analytics privacy-first
  • Tu veux un monitoring zero-config des Web Vitals
  • Une analytics de base suffit pour ton cas d'usage
  • Le budget permet un plan payant pour un trafic plus important

Quand envisager des alternatives :

  • Tu as besoin d'une segmentation avancée des utilisateurs
  • Tu exiges de l'analyse d'entonnoirs et de la modélisation d'attribution
  • Tu as un trafic très élevé (> 1M de pages vues/mois)
  • Tu as besoin d'une intégration avec des plateformes publicitaires
  • Tu préfères une solution self-hosted

Vercel Analytics est la solution idéale pour la plupart des applications Next.js, offrant un excellent équilibre entre simplicité, performance et fonctionnalités. Combiné à Speed Insights, il crée un système complet de monitoring des performances de l'application en temps réel !

Monitoring personnalisé avec Prometheus et Grafana

Pour un monitoring plus avancé, tu peux configurer ta propre stack avec Prometheus et Grafana.

1. Configuration des métriques Prometheus

1// lib/metrics.ts
2import client from 'prom-client';
3
4// Registre des métriques
5const register = new client.Registry();
6
7// Métriques HTTP
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// Métriques métier
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// Enregistre les métriques
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 pour le suivi HTTP

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 le traitement de la requête
9  const response = NextResponse.next();
10
11  // Enregistre les métriques après le traitement de la requête
12  response.headers.set('x-response-time', (Date.now() - start).toString());
13
14  // Enregistrement async des métriques (ne bloque pas la réponse)
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 pour les métriques Prometheus

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 et notifications

Un aspect important du monitoring est la notification automatique des problèmes.

1. Intégration avec 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 de l'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 et visualisation des données

1. Un dashboard simple dans 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}

Bonnes pratiques de monitoring

1. Stratégies de sampling

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% pour les utilisateurs gratuits
17    premium: 0.25,  // 25% pour premium
18    enterprise: 1.0, // 100% pour enterprise
19  };
20
21  return shouldSample(rates[userTier]);
22}

2. Optimisation des performances du monitoring

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 secondes
8
9  constructor() {
10    // Vidage automatique du buffer
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();

Résumé

Le monitoring des performances et l'analyse des erreurs sont des éléments clés de toute application Next.js en production. Une approche complète du monitoring comprend :

  1. Suivi des erreurs - détection et rapport automatiques des problèmes
  2. Monitoring des performances - Core Web Vitals et autres métriques clés
  3. Alerting - notifications immédiates en cas de problèmes critiques
  4. Dashboards - visualisation des données pour la prise de décision
  5. User analytics - comprendre comment les utilisateurs utilisent l'application

Au moment de choisir des outils de monitoring, prends en compte :

  • Sentry - pour un suivi complet des erreurs
  • Vercel Analytics - pour un monitoring simple sur Vercel
  • New Relic/DataDog - pour un monitoring avancé des performances
  • Custom solutions - pour des besoins spécialisés

N'oublie pas l'équilibre entre la profondeur du monitoring et son impact sur les performances de l'application. Utilise le sampling, le buffering et optimise les requêtes pour que le monitoring n'affecte pas négativement l'expérience utilisateur.

Code de cette leçon : App.tsx
1import React, { useState, useEffect } from 'react';
2
3// Demo : Monitoring des performances et analyse des erreurs
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' }}>Monitoring des performances et suivi des erreurs dans Next.js</p>
71
72      <div style={{ display: 'flex', gap: '8px', marginBottom: '24px' }}>
73        {[
74          { id: 'errors' as const, label: 'Erreurs (' + errors.filter(e => e.status === 'new').length + ')' },
75          { id: 'performance' as const, label: 'Performances' },
76          { id: 'setup' as const, label: 'Configuration' },
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            Simuler une erreur
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: &lt;{m.threshold.good}{m.unit} | Poor: &gt;{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;

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. Quelles métriques des Core Web Vitals Google utilise-t-il pour évaluer les performances d'une page ?

Tâches pratiques dans le jeu

  • Éditeur de code

    Crée un système de monitoring d'application : métriques de performance, suivi des erreurs, monitoring de disponibilité à l'aide des Web Vitals

  • Tri vertical

    Place les éléments dans le bon ordre : choisir un outil → ajouter l'instrumentation → configurer les dashboards

  • Tri vertical

    Place les étapes de configuration d'un performance budget dans le bon ordre :

Articles utiles