SentryNext.jsNestJSMonitoring

Sentry avec Next.js et NestJS : monitoring d'erreurs et performance en production

Intégrer Sentry dans une stack Next.js 16 + NestJS pour traquer erreurs, performances et regressions en prod. Tutoriel complet avec source maps et release tracking.

AMAlexis Mouchon5 min de lecture

Une app en production sans monitoring d'erreurs, c'est comme conduire les yeux fermés. Vous croyez que tout va bien parce que personne ne se plaint, mais 80 % des utilisateurs qui rencontrent un bug ne vous le diront jamais. Ils ferment l'onglet et vont voir ailleurs. Pour un site vitrine, c'est un prospect perdu. Pour un SaaS, c'est un client qui ne se réabonnera pas.

J'ai mis du temps à m'y mettre sérieusement, et je l'ai regretté. Aujourd'hui, Sentry est la première dépendance que j'ajoute après le scaffold d'un projet Next.js + NestJS. Trente minutes d'install, et vous voyez les erreurs en temps réel, le nom du fichier source exact, la ligne, la stack trace symbolisée, et la session utilisateur reproduite. Plus jamais sans.

Dans cet article, on va monter un setup propre, prod-ready, qui couvre les deux côtés de la stack — frontend Next.js 16 et backend NestJS — avec source maps, release tracking, et un tagging utilisateur cohérent.

Pourquoi Sentry plutôt qu'un logger maison ou Datadog

Posons les options sur la table.

Pour un projet entre 0 et 100k utilisateurs/mois, Sentry coche toutes les cases sans exploser la facture. Au-delà, vous garderez Sentry pour l'erreur frontend et ajouterez peut-être Datadog pour l'APM lourd côté infra.

Setup côté Next.js 16

Sentry maintient un SDK Next.js qui gère App Router, Server Actions, Server Components, et l'edge runtime. L'installation est scriptée.

npx @sentry/wizard@latest -i nextjs

Le wizard crée trois fichiers de config (sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts), patche next.config.ts, et ajoute votre DSN dans .env. Pas besoin de tout faire à la main, mais regardons ce qu'il produit pour bien comprendre.

Configuration client

// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
  environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? "development",

  // Traçage des performances (10% des sessions en prod)
  tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,

  // Session Replay : on capture 10% des sessions normales,
  // mais 100% de celles qui contiennent une erreur
  replaysSessionSampleRate: 0.1,
  replaysOnErrorSampleRate: 1.0,

  integrations: [
    Sentry.replayIntegration({
      maskAllText: false,
      blockAllMedia: true,
    }),
  ],

  // Filtrage : on ignore les erreurs réseau dues à un user offline
  beforeSend(event, hint) {
    const error = hint.originalException;
    if (error instanceof Error && error.message.includes("NetworkError")) {
      return null;
    }
    return event;
  },
});

Trois points méritent attention.

D'abord, tracesSampleRate : à 1.0 en dev pour tout voir, à 0.1 en prod pour ne pas exploser le quota. Sentry facture par span de transaction au-delà de 100k/mois.

Ensuite, Session Replay : c'est l'arme secrète. Quand une erreur se produit, vous obtenez une rediffusion vidéo (DOM rejoué, pas une vraie vidéo) des 30 secondes précédant le crash. Vous voyez l'utilisateur cliquer, taper, naviguer, puis l'erreur survenir. Plus besoin de "reproduisez-moi le bug s'il vous plaît" — vous le voyez en direct.

Enfin, beforeSend : permet de filtrer le bruit. Les erreurs NetworkError sont quasiment toujours dues à un utilisateur qui passe en mode avion. Inutile de payer pour les compter.

Configuration serveur

// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.VERCEL_ENV ?? "development",
  tracesSampleRate: process.env.NODE_ENV === "production" ? 0.2 : 1.0,

  // Capture les valeurs des Server Actions (utile pour debug)
  // ATTENTION : peut contenir des données sensibles, à filtrer
  beforeSend(event) {
    if (event.request?.data && typeof event.request.data === "object") {
      const data = event.request.data as Record<string, unknown>;
      if (data.password) data.password = "[REDACTED]";
      if (data.token) data.token = "[REDACTED]";
    }
    return event;
  },
});

Le serveur Next.js (Server Components, Server Actions, Route Handlers) tourne dans un contexte différent du browser. Cette config-là capture les erreurs de rendu serveur. On filtre les champs sensibles avant envoi — Sentry stocke les payloads, et vous ne voulez pas que vos mots de passe terminent dans un dashboard SaaS.

Patch de next.config.ts

Le wizard wrap votre config avec withSentryConfig :

import { withSentryConfig } from "@sentry/nextjs";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  reactStrictMode: true,
  experimental: {
    serverActions: { bodySizeLimit: "2mb" },
  },
};

export default withSentryConfig(nextConfig, {
  org: "votre-org",
  project: "alexis-portfolio",

  // Upload des source maps à chaque build (essentiel)
  silent: !process.env.CI,
  widenClientFileUpload: true,

  // Masque les requêtes Sentry derrière un proxy /monitoring
  // pour contourner les adblockers
  tunnelRoute: "/monitoring",

  // Désactive Sentry en dev pour ne pas spammer le free tier
  disableLogger: true,
  automaticVercelMonitors: true,
});

Le tunnelRoute est important. Beaucoup d'adblockers (uBlock Origin, Brave Shields) bloquent les requêtes vers *.sentry.io. En utilisant un tunnel, les events transitent par votre propre domaine, et vous capturez les utilisateurs qui auraient sinon été invisibles. Sur certains projets, j'ai gagné 30 % de visibilité juste avec ça.

Setup côté NestJS

NestJS n'a pas de SDK officiel Sentry, mais l'intégration est triviale avec @sentry/node et un Interceptor.

npm install @sentry/node @sentry/profiling-node

Initialisation dans main.ts

L'init doit avoir lieu avant la création de l'app Nest, sinon les erreurs du bootstrap échappent au capteur.

// src/main.ts
import * as Sentry from "@sentry/node";
import { nodeProfilingIntegration } from "@sentry/profiling-node";
import { NestFactory } from "@nestjs/core";
import { AppModule } from "./app.module";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV ?? "development",
  integrations: [nodeProfilingIntegration()],
  tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
  profilesSampleRate: 1.0,
  release: process.env.SENTRY_RELEASE,
});

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(process.env.PORT ?? 3001);
}

bootstrap();

Interceptor global pour capturer les exceptions

// src/common/interceptors/sentry.interceptor.ts
import {
  CallHandler,
  ExecutionContext,
  Injectable,
  NestInterceptor,
} from "@nestjs/common";
import * as Sentry from "@sentry/node";
import { GqlExecutionContext } from "@nestjs/graphql";
import { Observable, throwError } from "rxjs";
import { catchError } from "rxjs/operators";

@Injectable()
export class SentryInterceptor implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {
    return next.handle().pipe(
      catchError((exception: unknown) => {
        Sentry.withScope((scope) => {
          // Contexte GraphQL : on enrichit avec le nom de la query
          if (context.getType<"graphql">() === "graphql") {
            const gqlCtx = GqlExecutionContext.create(context);
            const info = gqlCtx.getInfo();
            scope.setTag("graphql.operation", info.fieldName);
            scope.setContext("graphql", {
              operationName: info.operation.name?.value,
              fieldName: info.fieldName,
            });
          }

          // Récupération de l'utilisateur authentifié
          const req = context.switchToHttp().getRequest();
          const user = req?.user;
          if (user) {
            scope.setUser({ id: user.id, email: user.email });
          }

          Sentry.captureException(exception);
        });

        return throwError(() => exception);
      }),
    );
  }
}

Branché globalement dans AppModule :

import { Module } from "@nestjs/common";
import { APP_INTERCEPTOR } from "@nestjs/core";
import { SentryInterceptor } from "./common/interceptors/sentry.interceptor";

@Module({
  providers: [
    {
      provide: APP_INTERCEPTOR,
      useClass: SentryInterceptor,
    },
  ],
})
export class AppModule {}

Avec ce setup, toutes les exceptions non capturées (REST, GraphQL, WebSockets) remontent dans Sentry avec le contexte utilisateur et l'opération GraphQL associée. C'est la différence entre "il y a une erreur quelque part" et "le user 42 a planté la mutation createOrder à 14h32".

Source maps : la marche à ne pas rater

Une stack trace minifiée ressemble à at o (chunks/4231-a7b.js:1:8234). Inutile. Avec les source maps uploadées, ça devient at submitForm (src/components/ContactForm.tsx:47:12). Sans cette étape, Sentry est à moitié aveugle.

Le SDK Next.js gère ça automatiquement via withSentryConfig pour peu que vous fournissiez un SENTRY_AUTH_TOKEN dans vos variables d'environnement de build (Vercel ou GitHub Actions).

Pour NestJS, ajoutez une étape de build :

# package.json
"scripts": {
  "build": "nest build && sentry-cli sourcemaps inject ./dist && sentry-cli sourcemaps upload ./dist --release $SENTRY_RELEASE"
}

Et installez le CLI :

npm install --save-dev @sentry/cli

Le SENTRY_RELEASE est typiquement le SHA du commit ou la version du package.json. C'est ce qui permet à Sentry de savoir quelle version du code est responsable d'une erreur, et de détecter les régressions (une erreur résolue qui réapparaît trois releases plus tard).

Tagging utilisateur cohérent entre Next.js et NestJS

Pour que Sentry corrèle correctement les erreurs frontend et backend d'un même utilisateur, il faut taguer l'utilisateur identifié des deux côtés avec le même identifiant.

Côté Next.js, dès que l'auth est résolue :

// app/providers.tsx (ou un useEffect dans un layout)
"use client";

import * as Sentry from "@sentry/nextjs";
import { useSession } from "next-auth/react";
import { useEffect } from "react";

export function SentryUserBinding() {
  const { data: session } = useSession();

  useEffect(() => {
    if (session?.user) {
      Sentry.setUser({
        id: session.user.id,
        email: session.user.email ?? undefined,
      });
    } else {
      Sentry.setUser(null);
    }
  }, [session]);

  return null;
}

Côté NestJS, on l'a déjà fait dans l'interceptor. Résultat : dans le dashboard Sentry, vous pouvez filtrer par user.id et voir toutes les erreurs qu'un utilisateur précis a rencontrées, front et back confondus, sur les 90 derniers jours. Pour un support client efficace, c'est de l'or.

Alerting : ne pas se noyer dans les notifications

Le piège classique : on connecte Sentry à Slack, on reçoit 200 notifs par jour, on les ignore toutes, et trois semaines plus tard une vraie panne passe inaperçue. L'alerting doit être agressif sur la rareté, pas sur le volume.

Mes règles par défaut :

Cette config-là transforme Sentry en outil utile au lieu d'un générateur de spam. Je la configure dans Alerts → Issue Alerts dès le premier déploiement.

Coûts réels en prod

Pour donner un ordre d'idée concret, voici ce que je consomme sur un projet typique (SaaS ~5 000 utilisateurs/mois, Next.js + NestJS) :

Total : ~52 $/mois pour une visibilité complète sur le frontend et le backend. À comparer aux 200+ $/mois de Datadog équivalent, ou aux heures passées à debugger sans contexte. ROI immédiat.

Conclusion

Mettre Sentry en place sur Next.js + NestJS prend une demi-journée, et change radicalement votre rapport à la production. Vous passez de "j'espère que ça marche" à "je sais exactement ce qui s'est cassé, pour qui, à quel moment, et pourquoi". Pour un site vitrine, c'est rassurant ; pour un SaaS, c'est vital.

Les trois pièges à éviter : ne pas oublier les source maps (sinon les stack traces sont illisibles), filtrer les données sensibles dans beforeSend (mots de passe, tokens), et calibrer le sampling pour ne pas exploser le quota dès le premier mois. Tout le reste est du raffinement.

Vous avez un projet web qui mérite un monitoring sérieux dès le premier jour ? N'hésitez pas à me contacter pour en discuter — un setup propre dès le départ coûte toujours moins cher qu'un debug en mode pompier six mois plus tard.


À lire aussi :