src/lib/sentry.ts

function·app·4.5 KB · 157 lignes· Voir l'itinéraire
Annotation non disponible

Lance npm run annotate (nécessite ANTHROPIC_API_KEY dans .env.local) pour générer une annotation française par Claude Haiku 4.5.

6 exports

setRequestUsercaptureErrorcaptureMessagewithSentrySentryContextSentryRequestUser

Code source· typescript

/**
 * Helpers Sentry backend wari.pro.
 *
 * `instrumentation.ts` init le SDK au démarrage Next.js 16 (server + edge).
 * Ici on expose des helpers user-friendly à utiliser dans les routes API.
 *
 * Usage :
 *   import { captureError, captureMessage } from "@/lib/sentry";
 *   try { ... } catch (e) { captureError(e, { route: '/api/...', tenantId }); }
 *
 * Le tag user est posé automatiquement par `getSessionFromRequest()` via
 * `setRequestUser(session)` — toute route qui résout la session enrichit
 * Sentry pour la durée de la requête. Les tags PER capture (ctx) override
 * les tags request si conflit.
 */
import * as Sentry from "@sentry/nextjs";
import { isBetaTester } from "@/lib/beta-testers";

export type SentryContext = {
  route?: string;
  tenantId?: string | null;
  userId?: string | null;
  role?: string | null;
  email?: string | null;
  extra?: Record<string, unknown>;
};

export type SentryRequestUser = {
  userId: string;
  email?: string | null;
  role: string;
  tenantId?: string | null;
} | null;

/**
 * Pose le user Sentry pour la durée de la requête courante (scope global
 * implicite). Doit être appelé après extract de la session — ce que fait
 * `getSessionFromRequest()` automatiquement. Aucun besoin d'appeler ce
 * helper depuis les routes individuelles.
 *
 * Si l'email matche `BETA_TESTER_EMAILS` :
 *   - `setTag('beta_tester', 'true')`
 *   - `setTag('environment', 'beta')` (override NODE_ENV pour ce user)
 *
 * Safe à appeler même si SENTRY_DSN absent (no-op gracieux).
 */
export function setRequestUser(session: SentryRequestUser): void {
  if (!process.env.SENTRY_DSN) return;
  try {
    if (!session) {
      Sentry.setUser(null);
      return;
    }
    Sentry.setUser({
      id: session.userId,
      ...(session.email ? { email: session.email } : {}),
    });
    Sentry.setTag("role", session.role);
    if (session.tenantId) Sentry.setTag("tenantId", session.tenantId);
    if (isBetaTester(session.email)) {
      Sentry.setTag("beta_tester", "true");
      Sentry.setTag("environment", "beta");
    }
  } catch {
    // Sentry ne doit jamais casser le flow applicatif
  }
}

/**
 * Capture une exception avec contexte enrichi.
 * Safe à appeler même si SENTRY_DSN absent (no-op gracieux).
 */
export function captureError(error: unknown, ctx: SentryContext = {}): void {
  if (!process.env.SENTRY_DSN) return;
  try {
    Sentry.withScope((scope) => {
      if (ctx.route) scope.setTag("route", ctx.route);
      if (ctx.tenantId) scope.setTag("tenantId", ctx.tenantId);
      if (ctx.role) scope.setTag("role", ctx.role);
      if (ctx.userId) {
        scope.setUser({
          id: ctx.userId,
          ...(ctx.email ? { email: ctx.email } : {}),
        });
      }
      if (isBetaTester(ctx.email)) {
        scope.setTag("beta_tester", "true");
        scope.setTag("environment", "beta");
      }
      if (ctx.extra) {
        for (const [k, v] of Object.entries(ctx.extra)) {
          scope.setExtra(k, v);
        }
      }
      Sentry.captureException(error);
    });
  } catch {
    // Sentry ne doit jamais casser le flow applicatif
  }
}

/**
 * Capture un message informatif (warning / info breadcrumb-style).
 */
export function captureMessage(
  message: string,
  level: "info" | "warning" | "error" = "info",
  ctx: SentryContext = {},
): void {
  if (!process.env.SENTRY_DSN) return;
  try {
    Sentry.withScope((scope) => {
      if (ctx.route) scope.setTag("route", ctx.route);
      if (ctx.tenantId) scope.setTag("tenantId", ctx.tenantId);
      if (ctx.role) scope.setTag("role", ctx.role);
      if (ctx.userId) {
        scope.setUser({
          id: ctx.userId,
          ...(ctx.email ? { email: ctx.email } : {}),
        });
      }
      if (isBetaTester(ctx.email)) {
        scope.setTag("beta_tester", "true");
        scope.setTag("environment", "beta");
      }
      if (ctx.extra) {
        for (const [k, v] of Object.entries(ctx.extra)) {
          scope.setExtra(k, v);
        }
      }
      scope.setLevel(level);
      Sentry.captureMessage(message);
    });
  } catch {
    // no-op
  }
}

/**
 * Wrapper async pour capturer automatiquement les erreurs d'une route.
 * Usage :
 *   export const GET = withSentry("/api/mobile/foo", async (req) => { ... });
 */
export function withSentry<T extends (...args: unknown[]) => Promise<unknown>>(
  route: string,
  handler: T,
): T {
  return (async (...args: Parameters<T>) => {
    try {
      return await handler(...args);
    } catch (error) {
      captureError(error, { route });
      throw error; // re-throw pour que Next.js retourne 500 standard
    }
  }) as T;
}