@laburen/delta-sdk
v1.3.0
Published
Instrumentación para los workers de Laburen en Cloudflare, sin dependencias de runtime. Puerta /telemetry: errores y eventos a la ingesta
Readme
@laburen/delta-sdk
Sirve para cualquier worker, MCP o workflow de Cloudflare.
El paquete de Laburen para instrumentar workers de Cloudflare, sin dependencias de runtime. Está
organizado en puertas: cada una es un import distinto para un propósito distinto, todas
conviviendo en el mismo paquete.
Puertas
| Puerta | Para qué |
|---|---|
| @laburen/delta-sdk | la raíz: solo los tipos compartidos entre puertas (Envelope, Notify, Severity, Surface, TelemetryEvent) — sin runtime |
| @laburen/delta-sdk/telemetry | le manda los errores y eventos de tu worker a la ingesta de telemetría (telemetry.laburen.com), que los guarda en D1 y avisa por Slack al canal del cliente |
Instalar el paquete, la API key y el ctx son iguales para cualquier puerta: ver
docs/empezar.md.
telemetry
Le manda los errores de tu worker a la ingesta de telemetría (telemetry.laburen.com), que los
guarda en D1 y avisa por Slack al canal del cliente.
Lo que tenés que saber antes de empezar
captureExceptionnunca lanza y no se puedeawait. Un cliente de telemetría no puede romper la app que observa.- No hay reintentos. Si la ingesta está caída, el evento se descarta y se cuenta.
- Los wrappers solo ven lo que ESCAPA de la función que envuelven. Si tu
catchloguea y devuelve un valor, el wrapper no se entera. Ver docs/telemetry/guia.md.
Caso 1 — MCP en un Durable Object (el 80% del uso)
import { createTelemetry } from "@laburen/delta-sdk/telemetry";
export class MiAgenteMCP extends McpAgent<Env> {
server = new McpServer({ name: "mi-mcp", version: "1.0.0" });
async init() {
const telemetry = createTelemetry({
service: { name: "mi-mcp", version: "1.0.0", environment: this.env.ENVIRONMENT },
surface: "durable_object",
organizationId: this.env.MI_ORGANIZATION_ID,
agentId: this.env.MI_AGENT_ID,
apiKey: this.env.LABUREN_TELEMETRY_KEY,
notify: { slack: { enabled: true, channel: this.env.MI_SLACK_CHANNEL_ID } },
ctx: this.ctx,
});
// La tool que NO atrapa su error: la envolvés y listo.
this.server.registerTool("MiTool", schema,
telemetry.withTool("MiTool", async (args) => {
const token = await renovarToken(); // si lanza: se captura Y se re-lanza
return await hacerAlgo(args, token);
}),
);
// La tool que SÍ atrapa su error: sumás una línea adentro del catch que ya tenés.
this.server.registerTool("OtraTool", schema, async (args) => {
try {
return await hacerOtraCosa(args);
} catch (error) {
telemetry.captureException(error, { workUnit: "OtraTool" }); // ← lo único nuevo
return { content: [{ type: "text", text: `Ocurrió un error: ${error.message}` }], isError: true };
}
});
}
}
createTelemetryva DENTRO deinit(), nunca a nivel de módulo. Un valor aleatorio generado en el scope global hace fallar el deploy con "Disallowed operation called within global scope", ywrangler devlocal no lo detecta.
Y no envuelvas el
export defaultconwithTelemetryen un MCP. Aunque el worker tenga unfetchque rutea hacia el Durable Object, el framework de MCP atrapa el error de la tool y lo convierte en un resultado conisError: trueantes de que llegue ahí: envolver elexport defaultno reportaría nada. Acá vacreateTelemetry+withTool, y nada más. Ver docs/telemetry/guia.md.
Caso 2 — Worker plano, cola y cron
import { withTelemetry } from "@laburen/delta-sdk/telemetry";
export default withTelemetry(
(env: Env) => ({
service: { name: "mi-worker", version: "1.0.0" },
organizationId: env.MI_ORGANIZATION_ID,
agentId: env.MI_AGENT_ID,
apiKey: env.LABUREN_TELEMETRY_KEY,
notify: { slack: { enabled: true, channel: env.MI_SLACK_CHANNEL_ID } },
queueMaxRetries: 3, // el MISMO número del wrangler.jsonc, sin sumarle 1
}),
{
async fetch(req, env, ctx, telemetry) { /* ... */ }, // → surface "worker"
async queue(batch, env, ctx) { /* ... */ }, // → surface "queue"
async scheduled(ctrl, env, ctx) { /* ... */ }, // → surface "cron"
},
);La config no lleva surface: se deduce del nombre del handler, y pasarla no compila.
El cuarto argumento (telemetry) es el mismo cliente que usa el wrapper. Un handler que
declara solo tres lo ignora y sigue andando igual; uno que lo declara puede usarlo en su propio
catch sin llamar a createTelemetry de nuevo. Ver docs/telemetry/guia.md.
Los nombres de las env var no están estandarizados entre proyectos. Abrí tu
env.d.tsantes de copiar los nombres: no asumas que son los mismos que en otro worker.
Caso 3 — Un error que nunca fue una excepción
Es el caso más común en los repos que ya existen: if (!res.ok) { log; return null }. No hay nada
que pasarle a captureException, así que va captureEvent.
if (!res.ok) {
telemetry.captureEvent("mi-integracion: crearTicket devolvió no-2xx", {
workUnit: "crear-ticket",
attributes: { status: res.status, ticket_id: ticketId }, // ← los ids van ACÁ
});
return null;
}El message va sin interpolar ids. Si el mensaje lleva ticket:157249, cada ocurrencia genera
un grupo nuevo y el dedup de la ingesta no sirve.
Documentación
| Archivo | Para qué |
|---|---|
| docs/empezar.md | instalar, la API key, el ctx, y qué puerta usar — transversal a cualquier puerta |
| docs/superficies.md | qué cambia entre worker, durable object, workflow, queue, alarm y cron — transversal a cualquier puerta |
| docs/troubleshooting.md | "no llega nada", error 1042, 401, "el worker no deploya" — síntomas compartidos por cualquier puerta |
| docs/telemetry/guia.md | la guía larga de /telemetry: config campo por campo, CaptureOptions, scrubbing, beforeSend |
| docs/telemetry/que-se-manda.md | el contrato de /telemetry en prosa, con un payload de ejemplo |
| docs/telemetry/troubleshooting.md | síntomas propios de /telemetry: 413, los límites del scrubbing |
El TypeScript real está en node_modules/@laburen/delta-sdk/src/: si algo no cierra, se lee.
Versionado
El paquete es additive-only: se agregan funciones y campos opcionales, nunca se renombra ni se
saca nada. Un worker que se queda en 1.2.0 tres años sigue funcionando. En la práctica el
paquete vive en 1.x y los bumps son minor y patch. El CHANGELOG dice, en cada
entrada, qué cambió y si hay que hacer algo.
