Capturar los errores de tu aplicación
Integra la captura de errores con los SDK en dos líneas, sin que el envío pueda romper tu aplicación, y resuelve o silencia lo que llega a UTILIA OS.
Actualizado el . 13 min de lectura
Incluido en todos los planes. Ver precios
Con dos líneas, tu aplicación envía a UTILIA OS los errores que nadie trata y los que tú atrapas. Llegan agrupados a Actividad › Errores, en la ficha de tu aplicación, donde tu equipo los resuelve, los silencia o los convierte en un ticket, y tu servidor recibe un aviso por cada cambio. La captura nunca lanza: si UTILIA OS no responde o falta un permiso, tu aplicación sigue exactamente igual.
De un fallo a tu equipo
install y capture
Capturar
El SDK saca el mensaje, la pila y dónde nació el fallo.Tu aplicación
Poner en cola
Junta los repetidos y envía en lotes de hasta 25.UTILIA OS
Agrupar
Suma la ocurrencia al grupo del mismo fallo y limpia el contexto.
No lo confundas con UtiliaSDKError, el error que lanza el SDK cuando falla una llamada a la API.
Antes de empezar
La captura llega con la versión 4.6.0 de los dos SDK (Versiones y compatibilidad). Tu aplicación externa necesita el permiso de cada cosa que hagas:
| Para | Permiso de la aplicación |
|---|---|
Enviar errores: install, capture, report y reportBatch | «Reportar errores al backend» (analytics:errors:write) |
| Listar, contar y ver el detalle | «Leer errores reportados» (analytics:errors:read) |
| Resolver, reabrir y silenciar | «Gestionar errores reportados» (analytics:errors:manage) |
Los permisos se conceden en la ficha de la aplicación: lo explica Permisos de la API.
En el navegador, la clave queda a la vista
Cualquiera puede leer la clave en el código de tu página. Para capturar errores en el navegador, usa una aplicación externa dedicada solo a eso, con «Reportar errores al backend» y ningún otro permiso, y declara tus dominios en sus Dominios autorizados (CORS). Nunca uses en el navegador la clave de una aplicación que factura o cobra.
Integración en dos líneas
La primera línea va al arrancar tu aplicación; la segunda, donde ya atrapas un error. El cliente se crea como siempre (Configurar el cliente).
Lo que conviene saber de estas dos líneas:
captureno lanza nunca ni hace falta esperarlo.captureycaptureMessage(en Python,capture_exceptionycapture_message) devuelven lo que contestó el servidor, onull(None) si el error se descartó o no se pudo enviar. En Python devuelven unconcurrent.futures.Future; en código asíncrono, si quieres esperarlo,await asyncio.wrap_future(futuro).- Saca solo los datos del error. El mensaje, con el tipo delante (
TypeError: …,ValueError: …), la pila y el archivo, la línea y la función del primer marco que es de tu código. En JavaScript entiende las pilas de Chrome, Node, Firefox y Safari y prefiere tu código al denode_modules. En Python,capture_exception()sin argumentos toma la excepción que se está tratando, así que va dentro de unexcept. captureMessage(capture_message) envía un aviso sin pila, para algo que no es una excepción pero quieres registrar.- Cada
captureadmitemodule,severity,context,tags(se suman a las deinstall),fingerprint,userId,requestId,endpointymethod.userIdes el identificador de la persona en tu sistema, no en UTILIA OS, y se guarda encontext.externalUserRef. installtampoco lanza. Llamarlo otra vez sustituye la configuración, y devuelve una función que la deshace: quita los ganchos y vuelve a los valores por defecto, aunque lo que ya está en la cola se sigue enviando.
Qué se captura solo
Con install, el SDK escucha lo que tu aplicación no trata:
| Dónde | Qué escucha | Gravedad con la que llega |
|---|---|---|
| Navegador | Los eventos error y unhandledrejection de la ventana | high |
| Node.js | uncaughtExceptionMonitor: excepciones sin tratar y, con el modo por defecto de Node, promesas rechazadas sin tratar | critical |
| Python | sys.excepthook | critical |
| Python | threading.excepthook y el manejador de excepciones de asyncio | high |
Los que capturas tú llevan la gravedad que indiques; si no indicas ninguna, se registran como low.
- No cambia cómo se comporta tu aplicación. En el navegador la consola sigue mostrando el error; en Node, el proceso termina igual que sin el SDK; en Python, el gancho anterior sigue funcionando y el traceback se imprime igual.
KeyboardInterruptySystemExitno se capturan. - En el navegador, el contexto lleva la dirección de la página sin la consulta ni el ancla (
context.url), y se ignoran los errores de scripts de otro dominio que llegan sin detalle («Script error.»). - En JavaScript, los capturados solos llevan
context.mechanism:window.onerror,unhandledrejection,uncaughtExceptionounhandledRejection. - En Python asíncrono, llama a
installcon el bucle deasyncioen marcha para que enganche su manejador de excepciones. - Tu framework puede llegar antes. Si Express, Django o FastAPI convierten el fallo en una respuesta 500, el error ya está tratado y no llega a estos ganchos: llama a
captureen el manejador de errores del framework.
Para no capturar nada solo y enviar únicamente lo que atrapas, pon captureUnhandled: false (capture_unhandled=False).
Dos matices de Node.js
Las promesas rechazadas. El SDK no añade un manejador unhandledRejection propio: con solo existir, Node deja de terminar el proceso ante una promesa rechazada sin tratar, y el SDK cambiaría el comportamiento de tu aplicación. Por eso depende de cómo arranques Node:
¿Llegan solas las promesas rechazadas?
¿Tu aplicación ya escuchaba unhandledRejection cuando llamaste a install?
Sí:
Llegan solas
El SDK añade su manejador junto al tuyo. No cambia nada: el tuyo sigue decidiendo qué pasa.No:
¿Arrancas Node con --unhandled-rejections=warn o none?
No:
Llegan solas
Con el modo por defecto, Node las convierte en excepciones y las recoge uncaughtExceptionMonitor.Sí:
No llegan solas
Llama a capture donde trates esas promesas.
La excepción que tumba el proceso. Cuando una excepción sin tratar va a terminar el proceso, Node lo cierra en el acto, sin dar tiempo a enviar nada: el error se queda en la cola y se pierde. Si quieres recibir también esos, añade tu propio manejador, que espere a la cola y salga:
process.on('uncaughtException', async (error) => {
console.error(error);
await sdk.errors.flush(2000);
process.exit(1);
});Con este manejador, la captura la sigue haciendo install; el tuyo solo da tiempo a enviarla. En Python no hace falta: install deja registrado un vaciado al salir del programa, de hasta 2 segundos, así que el error que lo tumba sí llega.
Sustituir el patrón anterior
No envíes errores desde unhandledRejection
El patrón process.on('unhandledRejection', () => void sdk.errors.report(...)) provoca un bucle: si el envío falla, su promesa rechazada vuelve a disparar el manejador, que vuelve a enviar, y el cupo se agota en segundos. Además, con ese manejador puesto, Node deja de terminar el proceso ante una promesa rechazada. Quítalo y llama a install.
// Antes: bucle si el envío falla
process.on('unhandledRejection', (reason) => {
void sdk.errors.report({ message: String(reason), module: 'servidor' });
});
// Ahora
sdk.errors.install({ module: 'servidor' });En Python, lo mismo con un manejador de asyncio que lanzaba sdk.errors.report en una tarea: quítalo y llama a install con el bucle en marcha.
Ajustar lo que se envía
Opciones de install, todas opcionales:
| JavaScript | Python | Por defecto | Qué hace |
|---|---|---|---|
release | release | Ninguna | Versión de tu aplicación, hasta 100 caracteres. |
environment | environment | Ninguno | Entorno (production, staging…), hasta 40 caracteres. Se guarda en minúsculas. |
module | module | app | Área de los errores capturados cuando capture no dice otra. |
tags | tags | Ninguna | Etiquetas comunes a todos los errores. |
captureUnhandled | capture_unhandled | Activado | Captura sola lo que nadie trata. |
beforeSend | before_send | Ninguna | Revisa cada error antes de enviarlo. |
sampleRate | sample_rate | 1 | Fracción de errores que se envían, de 0 a 1. |
maxQueue | max_queue | 100 | Errores que caben en la cola; lo que no cabe se descarta. |
flushIntervalMs | flush_interval | 2000 ms (2,0 s) | Cada cuánto sale un lote. En Python va en segundos. |
Versión y entorno
Con release, cada grupo guarda la primera y la última versión en que se vio: la lista de la pestaña Errores marca «Nuevo en 2.4.1», el listado se filtra por firstRelease y, si un error resuelto reaparece, el aviso dice en qué versión. El entorno no separa grupos: el mismo fallo en staging y en production cuenta como uno.
Agrupación propia
Dos errores son el mismo cuando coinciden el módulo, el mensaje normalizado y el primer marco de la pila que es de tu código (no de node_modules ni de las librerías de Python). Al normalizar, los identificadores largos (UUID, números de cuatro cifras o más, hexadecimales), las fechas, los correos, las IP y la consulta de las direcciones se cambian por un comodín, así que Pedido 48213 no encontrado y Pedido 51990 no encontrado caen en el mismo grupo. Los textos entre comillas y los números de tres cifras no se tocan, porque distinguen fallos distintos. La gravedad tampoco parte el grupo: guarda la más alta que ha visto.
Si sabes mejor que nadie qué es «el mismo error», manda fingerprint: una lista de 1 a 8 textos de hasta 200 caracteres. Los errores del mismo módulo con la misma lista forman un grupo, y el texto '{{default}}' dentro de la lista inserta la agrupación automática:
// Un grupo por fallo y por pasarela
sdk.errors.capture(error, { module: 'pagos', fingerprint: ['{{default}}', pasarela] });
// Un grupo por fallo y por entorno
sdk.errors.capture(error, { fingerprint: ['{{default}}', process.env.NODE_ENV ?? 'production'] });Etiquetas
tags es un objeto plano de hasta 20 etiquetas, con nombres de hasta 50 caracteres y valores de texto (hasta 200), número o booleano, sin anidar. Sirven para leer el error de un vistazo y se guardan en context.tags. Las de cada capture se suman a las de install.
Revisar antes de enviar
beforeSend (before_send) recibe cada error ya preparado y devuelve el mismo, cambiado o no, o null (None) para descartarlo. Úsalo para quitar datos que no quieres que salgan de tu aplicación o para callar el ruido. Si falla, ese error se descarta, y lo que llames a capture desde dentro se ignora.
sdk.errors.install({
beforeSend(event) {
if (event.message.includes('ResizeObserver loop')) return null; // ruido del navegador
if (event.context) delete event.context.telefono;
return event;
},
});Con sampleRate: 0.25 (sample_rate=0.25) sale, al azar, uno de cada cuatro errores; los demás devuelven null.
Qué te responde UTILIA OS
capture resuelve con accepted, status o reason, y hash, el identificador del grupo. Con accepted: true, status dice qué ha pasado:
created: un grupo nuevo. Tu servidor recibeERROR_REPORTED.updated: una ocurrencia más de un grupo que ya existía.regressed: el grupo estaba resuelto y se ha reabierto solo. Tu servidor recibeERROR_REOPENED.
Con accepted: false, reason dice por qué no se ha registrado. Ninguno es un fallo de tu aplicación:
reason | Qué significa | ¿Repetir? |
|---|---|---|
rate_limited | Más de 100 ocurrencias por minuto del mismo error. No se pierden: se suman al contador con la siguiente que entra. | No |
quota_exhausted | Tu aplicación ya ha registrado hoy 200 errores distintos. Las ocurrencias de los ya conocidos siguen entrando. | No |
overloaded | La plataforma se está protegiendo de una avalancha de errores. | No |
discarded_expected | El contexto trae un 4xx: un error de negocio que tu aplicación ya trata. Ver el apartado siguiente. | No |
storage_failed | No se pudo guardar. La cola del SDK lo repite sola, hasta tres veces. | Solo ese error |
Límites y conservación
Límites del registro de errores
- Peticiones por ruta y aplicación
- 120al minuto
- En cada ruta de /external/v1/errors.
- Ocurrencias del mismo error
- 100al minuto
- Las que pasan se suman al contador.
- Errores distintos por aplicación
- 200al día
- Por día natural en hora UTC.
- Conservación de cada grupo
- 14días
- Contados desde su última ocurrencia.
- El cupo por hora de tu clave no se toca. Las rutas de errores tienen su propio límite de 120 peticiones por minuto, y al pasarlo responden
429conRetry-After. La cola del SDK espera ese tiempo antes de repetir. - Los errores conocidos siempre entran. El tope de 200 al día cuenta solo los errores nuevos; un fallo que ya tiene grupo sigue sumando ocurrencias.
- El contexto se limpia al guardarlo. UTILIA OS quita las credenciales y los correos y lo recorta a 16 KB. Puedes enviar hasta 64 KB: el SDK ajusta antes de enviar los campos que se pasan de sus límites y, si el contexto supera unos 60 KB, lo sustituye por
contextTruncated: truey un extracto encontextPreview, para que un campo no haga rechazar el lote entero. - Los grupos sin resolver con un ticket o una tarea vinculados no caducan mientras sigan así.
Errores 4xx que no se registran
Si el contexto trae httpStatus, statusCode o errorCode con un código 4xx, UTILIA OS lo toma por un error de negocio o de permiso que tu aplicación ya trata, como un formulario mal rellenado o un 404 esperado, y no lo registra: accepted: false con reason: 'discarded_expected'. Los cortes pasajeros (408, 502, 503 y 504) sí se registran, con gravedad low.
Para registrar un 4xx que sí importa, como una dependencia que te responde 403, pon el código en context.upstreamStatus:
sdk.errors.captureMessage('La pasarela rechaza nuestra clave', {
module: 'pagos',
severity: 'high',
context: { upstreamStatus: 403, pasarela: 'redsys' },
});Enviar en lote por tu cuenta
install y capture ya envían en lote. Si prefieres controlar tú el envío, por ejemplo en un proceso que vuelca errores de un registro, usa reportBatch (report_batch): de 1 a 25 errores con los mismos campos que report, en un solo intento. Cada resultado trae su posición (index): repite solo los que vuelvan con reason: 'storage_failed'.
const errores = [
{ message: 'Factura sin cliente', module: 'importador', release: '2.4.1' },
{ message: 'Fecha fuera de rango', module: 'importador', release: '2.4.1' },
];
const { accepted, results } = await sdk.errors.reportBatch(errores);
const pendientes = results
.filter((r) => r.reason === 'storage_failed')
.map((r) => errores[r.index]);report envía un solo error y, a diferencia de capture, lanza UtiliaSDKError si algo falla. Un 503 con errorCode ERROR_STORAGE_UNAVAILABLE se puede repetir: espera lo que diga retryAfterMs (retry_after, en segundos). Los dos endpoints, con todos sus campos, están en Enviar los errores de tu aplicación.
Resolver, reabrir y silenciar
Con «Gestionar errores reportados» (analytics:errors:manage), tu aplicación gestiona sus propios errores igual que tu equipo en la pestaña Errores. Actúa en nombre de la persona responsable de la aplicación, que necesita el permiso Gestionar los errores de las aplicaciones externas; si no lo tiene, la respuesta es 403 APP_RESPONSIBLE_PERMISSION_DENIED.
const { data } = await sdk.errors.list({ status: 'active', severity: ['critical', 'high'] });
const detalle = await sdk.errors.get(data[0].hash); // pila, contexto y ticket vinculado
await sdk.errors.resolve(detalle.hash, 'Corregido en la 2.4.2');
await sdk.errors.silence(detalle.hash, 24); // de 1 a 672 horas (cuatro semanas)
await sdk.errors.unsilence(detalle.hash);
await sdk.errors.reopen(detalle.hash);Resolver uno ya resuelto devuelve alreadyResolved: true sin cambiar nada, y reabrir uno abierto, alreadyOpen: true. Un error silenciado sigue sumando ocurrencias, pero no cuenta como pendiente ni avisa hasta que vence el plazo.
Un error resuelto que vuelve a ocurrir se reabre solo. Esa ocurrencia responde status: 'regressed', el grupo suma una en regressionCount y tu servidor recibe ERROR_REOPENED con reason: 'regression' y la versión en release: así sabes, sin mirar, que la corrección no arregló nada. Los estados de un grupo y qué los cambia están en Estados de un grupo de errores, y todos los filtros y respuestas, en Consultar y gestionar los errores.
Avisos a tu servidor
Si suscribes tu aplicación a ellos, cada cambio llega como un aviso firmado:
ERROR_REPORTED: un grupo nuevo, concount,releaseyenvironment. No se repite por cada ocurrencia.ERROR_RESOLVED: alguien lo ha resuelto, desde la pestaña Errores, por la API o al cerrar el ticket o la tarea vinculados.ERROR_REOPENED: se ha reabierto, a mano (reason: 'manual') o porque volvió a ocurrir (reason: 'regression').ERROR_SILENCED,ERROR_LINKED_TO_TICKETyERROR_LINKED_TO_ISSUE: se silenció o se vinculó a un ticket o a una tarea.
El contenido de cada uno está en Avisos: contenido y catálogo de eventos.
Si algo falla
- No llega nada y la consola avisa una vez de
analytics:errors:write. La clave responde 401 o 403: concede «Reportar errores al backend» en la ficha de la aplicación. Mientras tanto, la cola descarta lo que intenta enviar. - En el navegador, las peticiones fallan con
403 ORIGIN_NOT_ALLOWEDo un error de CORS. Declara el dominio de tu web en los Dominios autorizados de la aplicación. capturedevuelvenull. El error se descartó porsampleRateobeforeSend, la cola estaba llena (maxQueue) o el envío falló tras sus reintentos.- Los errores que tumban un proceso de Node no llegan. Node cierra el proceso antes de enviar: añade el manejador de Dos matices de Node.js.
- Las promesas rechazadas de Node no llegan. Arrancas con
--unhandled-rejections=warnonone: llama acapturedonde las trates. - Los errores de tus rutas web no salen solos. Tu framework los convierte en respuestas 500 antes de que lleguen a los ganchos: llama a
captureen su manejador de errores. - Un error no aparece y la respuesta dice
discarded_expected. El contexto trae un 4xx; si te importa, usacontext.upstreamStatus. quota_exhaustedcasi a diario. Tus mensajes crean grupos distintos para el mismo fallo, normalmente por un dato variable que no se normaliza (un texto entre comillas, un número corto): quítalo del mensaje o agrupa confingerprint.