Skip to main content

Observabilidad

El panel usa dos herramientas. Sentry recoge errores, logs y trazas del frontend. PostHog registra eventos de producto y graba las sesiones. Las dos están enlazadas: cada evento de Sentry se puede relacionar con la sesión de PostHog en la que ocurrió. Esta página explica cuándo está activa cada una, qué se reporta y qué no, cómo se identifica al usuario y cómo pasan los errores de red por apiClient y React Query.

Sentry

Cuándo está activo

Además del entorno, hace falta VITE_SENTRY_DSN. Sin DSN, initSentry() sale sin hacer nada y todas las llamadas a Sentry son no-ops.
El entorno prod se reporta como production a propósito. Las reglas de alerta del proyecto de Sentry del panel filtran por production. Con prod, no se disparaba ninguna.

Arranque

src/main.tsx llama a initSentry() y a initAnalytics() antes de montar React. La configuración de src/config/sentry/index.ts incluye:
  • release: __APP_RELEASE__, con la forma panel-front@<sha>. Ver Entornos y despliegue.
  • browserTracingIntegration() para las trazas de navegación y de peticiones.
  • tracePropagationTargets: [VITE_API_BASE_URL]. El panel envía la cabecera sentry-trace a la API para enlazar trazas con el backend.
  • enableLogs más consoleLoggingIntegration con niveles warn y error. Cada console.warn y console.error llega a Sentry Logs. ESLint prohíbe el resto de console.* con la regla no-console.
  • sendDefaultPii: false.
El router se crea con Sentry.wrapCreateBrowserRouterV7(createBrowserRouter). Así Sentry nombra las transacciones por ruta. PortalRoot monta useSentryFeatureTag, que pone la etiqueta feature con el primer segmento de la ruta: homes, bookings, users… En /, la etiqueta es keys, porque ahí vive el dashboard de llaves. Sirve para filtrar incidencias y alertas por sección.

Release y source maps

En builds de staging y prod, vite.config.ts activa @sentry/vite-plugin:
  • Crea la release panel-front@<sha> y sube los source maps con SENTRY_ORG, SENTRY_PROJECT y SENTRY_AUTH_TOKEN.
  • Genera los mapas en modo hidden, sin referencia en el JS publicado.
  • Borra los .map del build después de subirlos.
La integración de Vercel con Sentry registra cada deploy en su release. Con eso Sentry distingue una incidencia nueva de una regresión.
El plugin se activa según VITE_APP_ENV, no según el modo de Vite, porque Vercel siempre construye con el modo production. Si VITE_APP_ENV no está bien configurado en Vercel, no se suben los source maps y los stack traces llegan minificados.

Qué se reporta y qué no

Los códigos silenciados son respuestas esperadas de la API: 41 (token inválido, ya cubierto por el flujo de sesión), 43 (sin permisos), 44 (no encontrado) y 49 (precondición de negocio). El 40 (petición mal formada) sí se reporta, porque suele ser un bug del panel.
Network Error está en la lista de ignorados de beforeSend. Aun así, los fallos de red de la API llegan a Sentry. captureNetworkError los etiqueta con network_error: api, y beforeSend deja pasar cualquier evento con esa etiqueta. Solo se descarta el ruido de red que no viene de apiClient.

Etiquetas y agrupación

Los fallos de red se agrupan con el fingerprint ["api-network-error", MÉTODO, ruta]. La ruta se normaliza: se quita el query string y los UUID y segmentos numéricos pasan a :id. Un endpoint caído genera una incidencia, no una por registro.

Sin reportes duplicados

Un mismo error puede pasar por apiClient, por React Query y por el error boundary. Para que llegue una sola vez, src/config/sentry/utils.ts guarda en un WeakSet los errores ya tratados:
  • captureException y captureNetworkError marcan el error al reportarlo.
  • apiClient también marca los errores que decide no reportar.
  • QueryCache, MutationCache y RouteErrorBoundary comprueban isErrorProcessed() antes de reportar. Si el error ya está tratado, el boundary muestra el ID con Sentry.lastEventId().

Datos sensibles

beforeSend filtra extra y las cabeceras de la petición. beforeBreadcrumb filtra los datos de los breadcrumbs xhr y fetch. En los tres casos, cualquier clave que contenga uno de estos textos pasa a valer [FILTERED]: password, token, access_token, refresh_token, authorization, api_key, apikey, secret o credential. El filtro es recursivo. Cada petición de apiClient deja además un breadcrumb http con el método y la URL.

Identidad del usuario

useAuth asocia el usuario cuando hay token. En Sentry llama a setSentryUser({ id, email }), donde id es el sub del token. Al cerrar sesión, o si el token desaparece, llama a clearSentryUser(). PostHog sigue el mismo ciclo, descrito más abajo.

Pantalla de error

Los dos boundaries muestran ErrorFallback (src/components/ErrorBoundary/index.tsx):
  • Un aviso de que el error se ha reportado automáticamente.
  • El enlace Comprueba si es un problema conocido, que abre la página de estado de VIVLA.
  • El ID del evento de Sentry con el botón Copiar. Con ese ID, cualquiera de tech encuentra la incidencia exacta.
  • El botón Volver a intentar. En el boundary del router recarga la página. En el raíz, resetea el boundary.
RouteErrorBoundary reporta dentro de un useEffect, no en el render. Así no se repite el reporte en cada re-render.

Errores de red

Todas las llamadas a la API pasan por apiClient (src/lib/apiClient.ts), una instancia de Axios con baseURL de VITE_API_BASE_URL y timeout de 30 segundos. React Query (src/config/queryClient.ts) hace de segunda red.
La API devuelve en el cuerpo un campo code propio: 20 es éxito. apiClient convierte cualquier otro código en un ApiError con code, msg y data. Así las mutaciones y queries reciben un error normal.
La captura global vive en MutationCache.onError y no en defaultOptions.mutations.onError. En TanStack Query v5, un onError definido en la mutación reemplaza al de defaultOptions. En cambio, el de MutationCache se ejecuta siempre. Casi todas las mutaciones del panel tienen su propio onError para mostrar un toast. No muevas la captura a defaultOptions.
El QueryClient usa staleTime de 60 segundos y refetchOnWindowFocus: false.

Última petición fallida

src/lib/lastFailedRequest.ts guarda en memoria la última petición de apiClient que falló: Solo se guarda la última, en una variable de módulo: una recarga la borra. El diálogo de reportes la adjunta al contexto y el hilo de Slack la muestra como Última petición fallida.
El envío de un reporte usa Axios directamente, no apiClient. Si falla, no pisa la última petición fallida. Sí llega a Sentry por MutationCache.

PostHog

Cuándo está activo

Además hace falta VITE_PUBLIC_POSTHOG_TOKEN. El host sale de VITE_PUBLIC_POSTHOG_HOST. Si PostHog no está activo, initAnalytics() instala un proveedor vacío (providers/noop.ts). Todas las llamadas funcionan sin hacer nada y getSessionReplayUrl() devuelve null. El resto del código no importa posthog-js. Todo pasa por la abstracción de src/lib/analytics/: trackEvent, trackPageView, setAnalyticsUser, clearAnalyticsUser y getSessionReplayUrl.

Configuración

providers/posthog.ts inicializa PostHog así:
  • capture_pageview: false. Las páginas vistas se envían a mano.
  • capture_pageleave: true.
  • persistence: "localStorage+cookie".
  • startSessionRecording() al arrancar: la grabación está siempre activa en staging y producción.
  • Propiedades globales en todos los eventos: app: "vivla-panel" y environment.
  • posthog.sentryIntegration se añade a Sentry con sendExceptionsToPostHog: false. Enlaza los eventos de Sentry con la sesión de PostHog sin duplicar las excepciones en PostHog.

Páginas vistas

PortalRoot monta useTrackPageView. En cada cambio de pathname o search, envía un $pageview con pageName (la ruta), search y $current_url. Solo se registran páginas del área autenticada.

Identificación

Cuando hay token, useAuth llama a setAnalyticsUser({ id, email }), que hace posthog.identify(id, { email }). El id es el sub del token de Auth0. Al cerrar sesión, clearAnalyticsUser() hace posthog.reset(). El proveedor admite también role, pero ahora mismo no se envía.

Grabaciones de sesión

getSessionReplayUrl() devuelve el enlace a la grabación actual, posicionado 30 segundos antes del momento de la llamada. Lo usa el diálogo de reportes: lo enseña como Grabación y lo adjunta al reporte.

Catálogo de eventos

Los eventos están tipados en src/lib/analytics/events.ts (AnalyticsEventMap). trackEvent solo acepta nombres de ese mapa, con sus propiedades exactas.
Para añadir un evento, decláralo primero en AnalyticsEventMap con sus propiedades y después llama a trackEvent. No llames a posthog.capture() directamente: te saltas el tipado y el proveedor vacío de local.
Los eventos de usuarios conservan el prefijo owner_* para no romper los dashboards de PostHog.
auth_login, auth_logout, booking_create_started y booking_create_step_viewed están en el mapa, pero ningún componente los emite. Si un dashboard depende de ellos, estará vacío.

Código