StackPractices
intermediate Por Mathias Paulenko

Seguimiento de Errores con Sentry en Express

Cómo integrar Sentry para seguimiento de errores en apps Node.js Express con el SDK v8+ — manejadores de error, spans de rendimiento, releases y source maps.

Visión General

Sentry captura los errores no manejados de tu aplicación, los agrupa por similitud y te avisa con el stack trace, el contexto de la petición, los datos del usuario y los breadcrumbs — los eventos que precedieron al error. En Express, el SDK instrumenta tus rutas automáticamente, así que cualquier error lanzado o promesa rechazada se convierte en un evento sin necesidad de try/catch por todas partes.

Esta receta usa @sentry/node v8+ — la API cambió bastante respecto a la v7 (el middleware Sentry.Handlers ya no existe). Los ejemplos de abajo funcionan con el SDK que obtienes hoy al hacer npm install @sentry/node.

Cuándo Usar

  • Aplicaciones en producción donde necesitas alertas de error en tiempo real
  • Seguir tasas de error y detectar regresiones entre releases
  • Capturar bugs reportados por usuarios con stack traces completos y contexto de la petición
  • Monitorizar el rendimiento (trazado de spans) junto al seguimiento de errores
  • Priorizar bugs por severidad, frecuencia y usuarios afectados

Cuándo No Usar

  • Desarrollo local — usa el depurador y la salida de consola
  • Aplicaciones sin manejo de errores — arregla primero tus bloques try/catch
  • Herramientas internas de poco tráfico — el plan gratuito de Sentry tiene límite de eventos
  • Solo envío de logs — Sentry registra errores y spans, no logs generales (para eso mira logging estructurado con Pino)

Solución

Instalación

npm install @sentry/node

Inicializa antes de cargar tu app

El SDK parchea http, express y otros módulos en el momento del import — así que Sentry.init() debe ejecutarse antes que el código de tu aplicación. La forma más limpia es un archivo instrument.js aparte:

// instrument.js
const Sentry = require("@sentry/node");

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  environment: process.env.NODE_ENV || "development",
  tracesSampleRate: 0.1,
});

Cárgalo primero en app.js:

require("./instrument"); // debe ser el primer require
const Sentry = require("@sentry/node");
const express = require("express");

const app = express();

app.get("/api/users/:id", (req, res) => {
  if (req.params.id === "0") {
    throw new Error("Invalid user ID");
  }
  res.json({ id: req.params.id, name: "Alice" });
});

// Regístralo después de todas las rutas — captura los errores que llegan hasta él
Sentry.setupExpressErrorHandler(app);

app.listen(3000);

En proyectos ESM, deja Sentry.init() en instrument.mjs y arranca con node --import ./instrument.mjs app.js — el flag garantiza el orden de importación.

Captura manual de errores

Sentry.captureException(new Error("Something went wrong"));

Sentry.captureMessage("Rate limit approaching", "warning");

try {
  JSON.parse("{invalid}");
} catch (err) {
  Sentry.captureException(err);
}

Añadir contexto a los errores

// Establece el usuario — se adjunta a cada evento hasta que se limpia
Sentry.setUser({
  id: 42,
  email: "alice@example.com",
  username: "alice",
});

// Los tags son filtrables en la UI de Sentry
Sentry.setTag("page_locale", "en-US");
Sentry.setTag("feature_flag", "new_checkout");

// El contexto extra es buscable pero no filtrable
Sentry.setExtra("order_id", "ord-123");
Sentry.setExtra("cart_items", 3);

// Limpia el usuario tras el logout
Sentry.setUser(null);
Sentry.addBreadcrumb({
  category: "auth",
  message: "User authenticated",
  level: "info",
  data: { method: "oauth", provider: "google" },
});

Sentry.addBreadcrumb({
  category: "http",
  message: "GET /api/users/42",
  level: "info",
  data: { status_code: 200, duration_ms: 45 },
});

// Si después ocurre un error, el evento muestra estos breadcrumbs

Span personalizado para monitorizar rendimiento

startTransaction desapareció en v8 — usa startSpan con un callback; el span termina solo cuando el callback acaba y los spans anidados pasan a ser hijos:

await Sentry.startSpan(
  { name: "process_order", op: "function" },
  async (span) => {
    await Sentry.startSpan({ name: "validate_payment", op: "task" }, async () => {
      // ... validar el pago ...
    });

    await Sentry.startSpan({ name: "save_order", op: "db" }, async () => {
      // ... guardar en la base de datos ...
    });

    span.setAttribute("order_id", "ord-123");
  }
);

Seguimiento de releases

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  release: process.env.SENTRY_RELEASE || "1.0.0",
  environment: process.env.NODE_ENV,
});

// Sentry ahora agrupa los errores por release y marca regresiones

Source maps para stack traces legibles

npm install @sentry/cli --save-dev

Sube los source maps durante el build:

sentry-cli sourcemaps upload --release 1.0.0 ./dist

O configúralos en sentry.properties:

defaults.url=https://sentry.io
defaults.org=your-org
defaults.project=your-project
auth.token=your-auth-token

Si puedes, mantén el token en SENTRY_AUTH_TOKEN — los tokens commiteados se filtran.

Integración con Winston

const winston = require("winston");
const Sentry = require("@sentry/node");

const sentryTransport = {
  log(info) {
    if (info.level === "error") {
      Sentry.captureException(new Error(info.message));
    }
    return true;
  },
};

const logger = winston.createLogger({
  transports: [new winston.transports.Console(), sentryTransport],
});

logger.error("Database connection failed");
// Envía tanto a la consola como a Sentry

Para una configuración de Winston lista para producción con rotación de archivos, mira Winston daily rotate.

Filtrado y muestreo

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  tracesSampleRate: 0.1, // 10% de las transacciones
  ignoreErrors: [
    "ResizeObserver loop limit exceeded",
    "Non-Error promise rejection captured",
  ],
  beforeSend(event) {
    if (event.request?.url?.includes("/health")) {
      return null; // No enviar el ruido del health check
    }
    return event;
  },
});

Para controlar qué errores de ruta captura setupExpressErrorHandler, pasa shouldHandleError a través de la integración:

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    Sentry.expressIntegration({
      shouldHandleError(error) {
        const status = Number(error.status ?? error.statusCode ?? 500);
        return status >= 500; // por defecto: solo los 5xx llegan a Sentry
      },
    }),
  ],
});

Explicación

El SDK v8 funciona por auto-instrumentación: al importar @sentry/node, envuelve http, express y otros módulos para que las peticiones entrantes, las llamadas salientes y los errores lanzados generen spans y eventos sin tocar tus handlers. Por eso importa el orden de carga — si Express se importa antes de Sentry.init(), los hooks de instrumentación se enganchan demasiado tarde y los errores pasan en silencio. El archivo instrument.js (o el flag --import en ESM) existe precisamente para garantizar ese orden.

setupExpressErrorHandler(app) registra el middleware de error de Sentry al final de tu cadena de middleware. Por defecto solo captura errores 5xx — los 4xx se consideran errores de cliente esperados y se ignoran, lo que reserva tu cuota para los fallos de verdad. shouldHandleError permite ampliar o estrechar ese filtro.

Un caso límite que conviene conocer: Express 4 no reenvía los errores lanzados dentro de handlers async al middleware de error. La integración de Sentry cubre la mayoría de los casos, pero si ves eventos que faltan en Express 4, envuelve los handlers con un asyncHandler (ver el FAQ) o actualiza a Express 5, que reenvía los rechazos de forma nativa.

El flujo es este:

flowchart diagram: Petición

tracesSampleRate controla solo la parte de rendimiento — los errores siempre se envían al 100%. Una tasa de 0.1 retiene el 10% de las transacciones, suficiente para detectar spans lentos sin quemar cuota. beforeSend se ejecuta sobre cada evento antes de salir de tu proceso; devolver null lo descarta, que es donde debe morir el ruido de health checks y scrapers.

Para correlacionar peticiones de extremo a extremo entre servicios, los spans que crea Sentry alimentan directamente el trazado distribuido.

Variantes

Usar Sentry con Fastify

Fastify tiene su propia integración en v8 — registra setupFastifyErrorHandler una vez, sin hooks manuales:

require("./instrument");
const Sentry = require("@sentry/node");
const Fastify = require("fastify");

const fastify = Fastify();

// Registra hooks que capturan errores de rutas y plugins
Sentry.setupFastifyErrorHandler(fastify);

fastify.get("/api/error", async () => {
  throw new Error("Fastify error");
});

Usar Sentry con TypeScript (ESM)

import * as Sentry from "@sentry/node";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [Sentry.httpIntegration()],
  tracesSampleRate: 0.1,
});

interface UserContext {
  id: string;
  email: string;
}

function setSentryUser(user: UserContext): void {
  Sentry.setUser(user);
}

Mejores Prácticas

  • Para una guía más completa, consulta Sentry: Seguimiento de Errores, Triage y Resolución.
  • Inicializa en un instrument.js aparte cargado antes de tu app — el orden de importación decide si la auto-instrumentación funciona.
  • Registra setupExpressErrorHandler(app) después de todas las rutas — solo ve los errores que llegan al final de la cadena.
  • Establece release con tu SHA de commit o tag de versión — te permite detectar regresiones entre despliegues.
  • Sube los source maps en CI — los stack traces minificados son inútiles sin ellos.
  • Usa beforeSend para descartar ruido (health checks, 404s de assets, errores de extensiones).
  • Pon tracesSampleRate a 0.1 en producción — 1.0 quema tu cuota de eventos rápido.
  • Usa setUser() para identificar quién sufrió el error — de oro a la hora de responder tickets de soporte.
  • Añade breadcrumbs en las acciones clave (auth, llamadas a API, cambios de estado) para reconstruir el camino hasta el error.

Errores Comunes

  • Importar Sentry después de la app: si Express carga antes que Sentry.init(), la auto-instrumentación nunca se engancha y los errores pasan sin reportar.
  • Registrar el manejador de error demasiado pronto: setupExpressErrorHandler antes de tus rutas solo captura errores del middleware que se ejecuta después.
  • Poner el DSN en el código: usa una variable de entorno. Commitear el DSN lo expone.
  • No subir los source maps: el código minificado produce stack traces ilegibles. Súbelos siempre en CI.
  • tracesSampleRate: 1.0 en producción: vale para staging, caro con tráfico real.
  • No limpiar el contexto de usuario en el logout: los errores posteriores al logout se atribuyen al usuario anterior.

Lectura Adicional

Código companion: proyecto starter de Express + Sentry — instrument.js, un app.js ejecutable con el manejador de error conectado y un .env.example.

Preguntas frecuentes

¿Cómo compruebo que Sentry funciona?

Crea una ruta de prueba:

app.get("/debug-sentry", () => {
  throw new Error("Sentry test error");
});

Visita la ruta y revisa tu panel de Sentry buscando el error.

¿Cuál es la diferencia entre captureException y captureMessage?

captureException recibe un objeto Error e incluye el stack trace. captureMessage recibe un string y crea un evento a nivel de mensaje. Usa captureException para errores y captureMessage para avisos o información.

¿Cómo manejo los errores asíncronos?

Express 5 reenvía las promesas rechazadas de los handlers async al middleware de error automáticamente. En Express 4, envuelve los handlers async para que los rechazos lleguen al manejador de error:

const asyncHandler = (fn) => (req, res, next) => Promise.resolve(fn(req, res, next)).catch(next);

app.get("/api/async", asyncHandler(async (req, res) => {
  const data = await fetchData();
  res.json(data);
}));
¿Puedo usar Sentry también para errores de frontend?

Sí. Instala @sentry/react o @sentry/vue para el frontend. Usa la misma organización y proyecto — Sentry separa los errores de frontend y backend por plataforma.

¿Cómo reduzco los costes de Sentry?
  • Baja tracesSampleRate (0.01 en apps de mucho tráfico)
  • Usa ignoreErrors y beforeSend para filtrar el ruido conocido
  • Mantén shouldHandleError en el filtro por defecto de 5xx para que los 4xx no consuman cuota
  • Pon maxBreadcrumbs a 20 en lugar de los 100 por defecto