Documentación

Estiaje recibe lo que pasó en tu código y lo que le importa a tu negocio en un mismo río. Esta página es todo lo que necesitas: se lee en quince minutos y no hay una segunda parte escondida.

Instalar

npm install @estiaje/sdk

Cero dependencias. Node 18 o superior. Funciona igual en una API, en un front empaquetado y en una lambda.

Tu primer evento

1

Consigue tu llave

Crea tu organización en la pantalla de alta. Te devuelve una llave ck_live_… que se muestra una sola vez: se guarda cifrada, así que si la pierdes se revoca y se genera otra.

2

Apunta el SDK

ESTIAJE_URL=https://tu-estiaje.example.com
ESTIAJE_TOKEN=ck_live_…

Sin ESTIAJE_URL el SDK queda apagado — todas las funciones existen y no hacen nada. Cero red, cero buffers. Puedes instrumentar producción hoy y decidir el destino después.

3

Emite

import { estiajeFromEnv } from '@estiaje/sdk';

// UNO por proceso. No uno por request, ni uno por archivo.
export const estiaje = estiajeFromEnv({ service: 'mi-api' });

estiaje.log('usecase.ok', { usecase: 'GetVisits', ms: 128 });

Ábrelo en el río y verás caer el evento en menos de dos segundos.

Los tres verbos

La diferencia no es de estilo: decide cuánto vive cada cosa y qué preguntas vas a poder contestar dentro de un año.

VerboPara quéVive
estiaje.log()Lo que pasó. Diagnóstico, tiempos, pasos internos.Se borra: es ruido con fecha de caducidad.
estiaje.event()Lo que le importa al negocio: una visita empezó, un planograma cambió.Se conserva.
estiaje.outcome()Lo que TERMINA un proceso: la venta se procesó, la tarea cerró.Se conserva. Es la desembocadura.

También hay warn(), error() y fatal(): son log con severidad.

Si solo usas log(), no te falta nada. Los otros dos verbos aparecen el día que quieras contestar "¿cuántos pedidos terminaron en cobro procesada?" sin abrir la base de datos.

Nombra por lo que ocurrió, no por dónde ocurrió

order.created, no PedidoController.done. El nombre sobrevive al refactor; la ruta del archivo, no. Y en los eventos de negocio incluye siempre el identificador de la cosa (task_id, order_id, sn): es lo que permite emparejar el inicio con el final.

Contexto y linaje

El hilo, la entidad, el actor y las etiquetas del request no se escriben en cada llamada. Se declaran una vez y se pegan solos a todo lo que se emita.

estiajeFromEnv({
  service: 'api-pedidos',
  context: () => ({
    trace_id: hiloActual(),        // del interceptor / AsyncLocalStorage
    entity:   'sucursal:polanco',     // el ancla: la cosa del mundo real
    actor:    usuarioActual(),
    attrs:    { session_id, module: 'checkout' },
    geo:      posicionActual(),
  }),
})

Si el front manda su cabecera traceparent y tu interceptor la respeta, la misma traza va del click del usuario hasta la lambda sin que ningún caso de uso escriba un trace_id jamás. Eso es lo que dibuja el viaje.

El hook context() se llama en cada emisión. Si lanza, se ignora — el SDK nunca falla por culpa del contexto.

Tus etiquetas nunca chocan con las del contexto

Si ambos traen la misma clave con distinto valor, gana la tuya y la del contexto se conserva con el sufijo _ctx. Nada se pisa en silencio.

CasoResultado
Solo el contexto la traequeda la del contexto
Solo tú la escribesqueda la tuya
Ambos, mismo valorse funden en una
Ambos, valores distintosmanda la tuya, la otra queda como <clave>_ctx

Etiquetas (attrs)

Los attrs son pares clave/valor con los que después vas a buscar. Pueden ser texto, número o booleano — y si mandas otra cosa, el SDK no tira el evento: lo degrada con gracia y lo marca.

Le mandasGuarda
Error"TypeError: x is not a function" + marca _coerced
DateISO 8601
objeto / arrayJSON truncado a 500 caracteres + _coerced
objeto circular"[no serializable]"
NaN / Infinitysu texto + _coerced
null / undefinedse omite (sin ruido)

El atributo _coerced lista qué claves se degradaron. La idea es que el dato sucio sea visible y corregible, no invisible. Si tienes un objeto grande que sí quieres conservar entero, va en el tercer parámetro (body), que se guarda como JSON completo.

Dónde pasó: geo

geo no es un attr: es parte del sobre, como entity o trace_id. Puede ir en el evento o en el contexto, y las dos formas terminan igual.

estiaje.event('order.created', {
  geo: { lat: 25.6866, lon: -100.3161 },  // acc opcional, en metros
  slots: 8,
});
CasoResultado
En el evento y en el contextomanda el del evento
lat/lon fuera de rango, o basurano sube, se conserva como geo_invalido
0,0se descarta: es un GPS sin fijar, no el Golfo de Guinea
Sin geoel evento existe igual, solo que no sale en el mapa

Solo lo que ocurre en un lugar real lo necesita: una visita, una venta, una tarea cerrada en sitio. Lo que pasa dentro del servidor, no. El territorio nunca inventa una posición: si un evento no la trae, lo dice.

Destinos (sinks)

Esta es la idea central del SDK: la declaración es estable y dura años; el destino es configuración. Manda a Estiaje, a la consola, a cualquier backend OTLP — Sentry, Datadog, Grafana — o a los cuatro a la vez.

import { createEstiaje, sinkConsole, sinkOtlp } from '@estiaje/sdk';

createEstiaje({
  service: 'mi-api',
  url: process.env.ESTIAJE_URL, token: process.env.ESTIAJE_TOKEN,
  sinks: [
    'estiaje',
    sinkConsole(),
    sinkOtlp('https://…ingest.sentry.io/api/…/otlp/v1/logs'),
    { name: 'mío', send: (evs) => miLogger.info(evs) },
  ],
})
Cada destino tiene su propia cola. Si Sentry está caído, Estiaje sigue recibiendo — y al revés. Un proveedor lento no contagia a los otros, y el que revive se pone al día solo. Un sink que lanza se aísla: jamás tumba al emisor.

Un sink tuyo solo necesita dos cosas:

interface EstiajeSink {
  name: string;
  send(events: EstiajeEvent[]): void | Promise<void>;
  immediate?: boolean;   // true = sin esperar al lote (así es la consola)
}

Variables de entorno

VariableQué hace
ESTIAJE_URLEl endpoint. Sin ella el SDK queda apagado.
ESTIAJE_TOKENTu llave ck_live_…
ESTIAJE_SINKSestiaje,console,otlp — separados por coma
OTLP_ENDPOINTEl endpoint OTLP del proveedor
OTLP_HEADERSCabeceras extra, k=v,k=v
ESTIAJE_ECHO0 apaga la copia a consola (viene encendida)

La copia a consola está encendida a propósito: se ve en desarrollo, y en AWS CloudWatch la captura sin configurar nada. Se apaga con ESTIAJE_ECHO=0 o echo: false.

Expectativas

Un error se ve. Un proceso que se quedó a la mitad no hace ruido. Las expectativas son la forma de enterarte del silencio sin escribir una línea de código extra.

Se declaran donde vive la operación, no en el código:

{
  "id":         "pedido-completo",
  "name":       "Pedido creado → pedido completado",
  "when":       "order.created",
  "expects":    "order.completed",
  "fails":      ["sales.rejected", "etl.failed"],
  "match_by":   "attrs.order_id",
  "window_min": 30,
  "severity":   "error"
}

Cada abridor termina en uno de tres finales, y solo uno se anuncia:

FinalQué pasa
Cumplida — llegó lo de expectsse cierra en silencio
Fallida declarada — llegó algo de failsse cierra y se cuenta aparte: tu código detectó el problema y lo dijo, eso vale oro
Sin cierre — no llegó nadase anuncia como expectation.unmet
No hay cronómetros ni flujos que registrar. Es una consulta que corre periódicamente y compara aperturas contra cierres. Es idempotente por derivación: el mismo abridor no se anuncia dos veces, aunque la reconciliación corra mil veces.

Y si tu código detecta que el proceso se truncó, dilo: estiaje.outcome('task.canceled', { task_id }). Un final malo declarado es información; lo que no sirve es el silencio.

Retención y cuotas

Los log son ruido y se borran según tu plan. Los event y outcome se conservan, porque son los que contestan preguntas de negocio meses después. Además hay un rollup por minuto que sobrevive al borrado: el volumen y la salud de cada servicio siguen ahí aunque el detalle ya no esté.

La cuota se cobra antes de leer el cuerpo de la petición — un lote enorme no te cuesta CPU si ya te pasaste. Si excedes, la puerta responde 429 con Retry-After, y el SDK reintenta solo.

API HTTP

El SDK es comodidad, no obligación. Todo se puede hacer con curl. La credencial va en Authorization: Bearer, y la organización sale siempre de la credencial — nunca del payload ni de un parámetro.

Mandar eventos

curl -X POST https://tu-estiaje/v1/events \
  -H 'content-type: application/json' \
  -H 'authorization: Bearer ck_live_…' \
  -d '[{"service":"mi-api","kind":"event","name":"order.created",
       "attrs":{"order_id":"AND-8821"}}]'

Acepta un objeto o un arreglo. Responde 202 con {accepted, deduped, rejected[]}: un evento malo no tumba el lote — se rechaza solo él, con su índice y su razón.

Consultar

EndpointPara qué
GET /v1/eventsBuscar. Filtra por q (prefijo del nombre), service, kind, severity, entity, trace, attr=clave:valor, con_geo=1. Rangos relativos: desde=-24h. Paginado por cursor.
GET /v1/traces/:traceUn viaje leído como historia: los pasos en orden, cuánto tardó entre uno y otro, y cuál fue el salto más largo.
GET /v1/servicesQuién está vivo: volumen, mezcla, errores y silencio por servicio.
GET /v1/latencyp50/p95/max de attrs.ms agrupado por cualquier etiqueta o columna. name=* = todo lo que traiga ms.
GET /v1/expectationsQué se está vigilando y cómo va cada una.
GET /v1/liveSSE: el río en vivo, un resumen por segundo.

Dos tipos de credencial

TipoQuiénPuede
ck_live_…un servicioemitir y consultar. Se guarda cifrada y se muestra una sola vez.
cs_…una personaentrar al panel. Dura 30 días. No puede emitir.

Qué NO hacer

La versión de treinta segundos. Importa y emite: log para lo que pasó, event para lo que le importa al negocio, outcome para lo que termina un proceso. En los eventos de negocio incluye el identificador de la cosa. Y si tu código detecta que algo se truncó, dilo. Eso es todo.