Node.js
Guia completo de integração com o ImmutableLog em Node.js e TypeScript. Escolha a abordagem que melhor se encaixa no seu projeto: middleware/plugin automático para frameworks web, integração direta com `fetch`, ou wrappers para instrumentar funções específicas.
O jeito mais rápido de integrar: o SDK oficial para Node.js
@immutablelog/sdk
Pacote oficial publicado no npm com um núcleo agnóstico e adapters prontos para Express, Fastify, Koa, NestJS, AdonisJS e Node puro. Fail-open (uma indisponibilidade nunca quebra sua app), entrega não-bloqueante, redação automática de dados sensíveis, hash SHA-256 do corpo e tipos TypeScript inclusos. Zero dependências de runtime.
Instalação
npm install @immutablelog/sdkInício rápido
import { imtbl } from '@immutablelog/sdk';
// Lê IMTBL_API_KEY / IMTBL_URL do ambiente
await imtbl.sendEvent({
eventName: 'payment.approved',
payload: { paymentId: 'pay_abc123', amount: 299.9 },
kind: 'success',
immutableTrail: 'order-7782',
});Exemplo com Express
import express from 'express';
import { immutableLog } from '@immutablelog/sdk/express';
const app = express();
app.use(express.json());
// Audita toda requisição automaticamente
app.use(immutableLog({ service: 'orders-api' }));
// ...suas rotas...
// Captura exceções (depois das rotas)
app.use(immutableLog.errorHandler({ service: 'orders-api' }));Versão beta (0.1.x). A API pública pode mudar antes da 1.0 — fixe a versão no seu package.json.
Middlewares para frameworks
A forma mais rápida de integrar o ImmutableLog é via middleware ou plugin nativo do framework. Toda requisição HTTP é capturada automaticamente — erros, sucessos, latência e contexto — sem alterar nenhuma rota. Clique no framework para ver o código completo pronto para usar.
Middleware function com `app.use()`. Captura via `res.on('finish')` e `res.on('error')`. Zero dependências extras.
import { immutableLogMiddleware } from './middleware/immutablelog';
app.use(immutableLogMiddleware({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'express-service',
}));Ver documentação completa →
Plugin com `fastify-plugin` e hooks `onRequest`, `onResponse`, `onError`. Suporta TypeScript type augmentation.
import immutableLogPlugin from './plugins/immutablelog';
await fastify.register(immutableLogPlugin, {
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'fastify-service',
});Ver documentação completa →
Interceptor com RxJS `tap`/`catchError`. Suporta registro global via `APP_INTERCEPTOR`, por módulo ou por controller.
// app.module.ts
ImmutableLogModule.forRoot({
apiKey: process.env.IMTBL_API_KEY!,
apiUrl: 'https://api.immutablelog.com',
serviceName: 'nestjs-service',
})Ver documentação completa →
Integração com fetch
Use a API nativa `fetch` do Node.js 18+ para enviar eventos diretamente ao ImmutableLog sem depender de um framework web. Ideal para workers, scripts, jobs e qualquer código Node.js que precise registrar eventos no ledger.
Função helper sendEvent
Crie uma função utilitária reutilizável para encapsular a lógica de envio. Totalmente tipada em TypeScript — serializa o payload, gera os headers obrigatórios e faz o POST ao endpoint de ingestão.
// src/lib/immutablelog.ts
import crypto from 'crypto';
const IMTBL_API_KEY = process.env.IMTBL_API_KEY ?? '';
const IMTBL_URL = process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
// Normaliza o immutable_trail (trim, nao-vazio, max 256, sem ':').
// O servidor responde 400 invalid_immutable_trail se a regra for violada.
function sanitizeTrail(value?: string | null): string | undefined {
if (typeof value !== 'string') return undefined;
let v = value.trim();
if (!v) return undefined;
if (v.includes(':')) v = v.replace(/:/g, '-');
if (v.length > 256) v = v.slice(0, 256);
return v;
}
export interface SendEventOptions {
eventName: string;
payload: Record<string, unknown>;
kind?: 'success' | 'error' | 'info' | 'warning';
service?: string;
env?: string;
// Agrupa eventos relacionados em uma trilha auditavel (ex: `flow-123`)
immutableTrail?: string;
// Timezone do cliente — usado pelo dashboard para exibir os timestamps
clientTz?: string;
clientOffsetMinutes?: number;
}
// Resposta da API de ingestao (assincrona)
export interface SendEventResult {
ok: boolean;
tx_id: string;
payload_hash: string;
status: string; // "accepted" | "committed"
duplicate: boolean; // true quando a Idempotency-Key ja existia (HTTP 200)
request_id?: string;
}
export async function sendEvent(opts: SendEventOptions): Promise<SendEventResult> {
const {
eventName, payload, kind = 'info', service = 'node-service', env = 'production',
clientTz = 'America/Sao_Paulo', clientOffsetMinutes = -180,
} = opts;
const requestId = crypto.randomUUID();
const trail = sanitizeTrail(opts.immutableTrail);
const payloadStr = JSON.stringify({
...payload,
timestamp: new Date().toISOString(),
});
const event = {
payload: payloadStr,
// IMPORTANTE: todos os valores de meta precisam ser strings.
meta: {
type: kind,
event_name: eventName,
service,
request_id: requestId,
env,
...(trail && { immutable_trail: trail }),
},
};
const res = await fetch(`${IMTBL_URL}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${IMTBL_API_KEY}`,
'Content-Type': 'application/json',
// Idempotency-Key e OBRIGATORIO (sem ele a API responde 400)
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
'X-Client-TZ': clientTz,
'X-Client-Offset-Minutes': String(clientOffsetMinutes),
...(trail && { 'X-Imtbl-Trail': trail }),
},
body: JSON.stringify(event),
signal: AbortSignal.timeout(5000),
});
// Sucesso: 202 (novo) ou 200 (duplicata idempotente). Erros lancam com o status.
if (!res.ok) {
const err = new Error(`ImmutableLog error: ${res.status}`) as Error & { status?: number };
err.status = res.status;
throw err;
}
return res.json() as Promise<SendEventResult>;
}
// Uso / Usage
await sendEvent({
eventName: 'payment.approved',
payload: { paymentId: 'pay_abc123', amount: 299.90, currency: 'BRL' },
kind: 'success',
service: 'payments-service',
immutableTrail: 'order-7782', // agrupa todos os eventos deste pedido
});Trilha imutável (immutable_trail)
A trilha agrupa eventos relacionados em uma mesma linha do tempo auditável — ideal para acompanhar um fluxo de negócio (`flow-123`), um pedido (`order-7782`) ou um usuário (`user-maria`). Passe `immutableTrail` em qualquer chamada de `sendEvent`; o valor é normalizado por `sanitizeTrail` e enviado tanto em `meta.immutable_trail` quanto no header `X-Imtbl-Trail`.
// Todos os eventos abaixo ficam na mesma trilha "order-7782"
// All events below share the same "order-7782" trail
await sendEvent({ eventName: 'order.created', payload: { orderId: 'ord_7782' }, immutableTrail: 'order-7782', kind: 'success' });
await sendEvent({ eventName: 'payment.approved', payload: { amount: 299.9 }, immutableTrail: 'order-7782', kind: 'success' });
await sendEvent({ eventName: 'order.shipped', payload: { carrier: 'ABC' }, immutableTrail: 'order-7782', kind: 'success' });
// sanitizeTrail aplica as regras do servidor antes do envio:
// sanitizeTrail('flow:42 ') -> 'flow-42' (remove espacos, troca ':' por '-')Regras do servidor: a trilha não pode ser vazia, exceder 256 caracteres ou conter `:`. Violações retornam `400 invalid_immutable_trail` — por isso `sanitizeTrail` normaliza o valor antes do envio.
Resposta e códigos de status
A ingestão é assíncrona. Sucesso retorna `202 Accepted` (evento enfileirado) ou `200 OK` quando a Idempotency-Key já existe (`duplicate: true`). O corpo traz `{ ok, tx_id, payload_hash, status, duplicate, request_id }`. A `Idempotency-Key` é obrigatória — sem ela a API responde `400`.
| Status | Significado |
|---|---|
| 202 | Evento aceito e enfileirado (novo) |
| 200 | Idempotency-Key já existente — duplicate: true |
| 400 | Idempotency-Key ausente/vazia ou invalid_immutable_trail |
| 403 | Assinatura inativa/expirada, escopo ou retenção |
| 413 | payload_too_large (limite do servidor: 16KB) |
| 429 | monthly_limit_exceeded — não fazer retry |
| 503 | mempool_full — transitório, pode dar retry |
Retry com backoff exponencial
Para produção com alta disponibilidade, adicione retry automático com backoff exponencial. O ImmutableLog retorna `202` em sucesso e `429` quando o limite mensal é atingido — nunca faça retry em 429.
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function sendEventWithRetry(
opts: SendEventOptions,
maxRetries = 3,
baseDelay = 500,
): Promise<{ tx_id: string } | null> {
for (let attempt = 0; attempt < maxRetries; attempt++) {
try {
return await sendEvent(opts);
} catch (err) {
const status = (err as { status?: number }).status;
// 429 = limite mensal — nao fazer retry / monthly limit — do not retry
if (status === 429) {
console.warn('[ImmutableLog] Monthly limit reached, skipping retry.');
return null;
}
if (attempt === maxRetries - 1) {
console.error('[ImmutableLog] Max retries reached:', err);
return null;
}
const delay = baseDelay * 2 ** attempt; // 500ms, 1s, 2s
console.warn(`[ImmutableLog] Retry ${attempt + 1}/${maxRetries} in ${delay}ms`);
await sleep(delay);
}
}
return null;
}Envio em lote (batch)
Em workers de alta frequência, acumule eventos e envie em paralelo usando `Promise.allSettled`. Cada evento ainda é enviado individualmente — use `Promise.allSettled` para paralelizar sem falhar no conjunto.
async function sendEventsBatch(events: SendEventOptions[]): Promise<void> {
const results = await Promise.allSettled(events.map((ev) => sendEvent(ev)));
results.forEach((result, i) => {
if (result.status === 'rejected') {
console.warn(`[ImmutableLog] Batch item ${i} failed:`, result.reason);
}
});
}
// Uso / Usage
await sendEventsBatch([
{ eventName: 'order.created', payload: { orderId: 'ord_1' }, kind: 'success' },
{ eventName: 'order.created', payload: { orderId: 'ord_2' }, kind: 'success' },
{ eventName: 'payment.failed', payload: { orderId: 'ord_3' }, kind: 'error' },
]);Wrappers de função
Wrappers permitem instrumentar funções específicas sem modificar sua lógica interna. Ideal para marcar operações críticas de negócio (ex: processar pagamento, criar usuário) onde você quer garantir rastreabilidade independente do framework usado.
Wrapper síncrono / async
Um único wrapper detecta automaticamente se a função é assíncrona via `fn.constructor.name`. O evento é enviado no bloco `finally` — garantindo o registro mesmo quando a função lança uma exceção.
function withAudit<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
service = 'node-service',
): T {
const isAsync = fn.constructor.name === 'AsyncFunction';
const name = eventName ?? `fn.${fn.name}`;
const handle = (startedAt: number, err: Error | null) => {
const latencyMs = Date.now() - startedAt;
const actualKind = err ? 'error' : kind;
const payload: Record<string, unknown> = {
id: crypto.randomUUID(),
kind: actualKind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
// fire-and-forget
sendEvent({ eventName: name, payload, kind: actualKind, service }).catch(() => {});
};
if (isAsync) {
return (async (...args: unknown[]) => {
const startedAt = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const startedAt = Date.now();
try { return fn(...args); }
catch (err) { handle(startedAt, err as Error); throw err; }
finally { handle(startedAt, null); }
}) as unknown as T;
}
// Uso / Usage
const processPayment = withAudit(
async (paymentId: string) => {
// ... logica de negocio / business logic ...
return { status: 'approved' };
},
'payment.process',
'success',
'payments-service',
);
await processPayment('pay_abc123');Classe AuditClient (configurável)
Para projetos maiores, use uma classe para centralizar a configuração. Instancie uma vez na inicialização e use `audit.wrap()` em qualquer função sync ou async.
import crypto from 'crypto';
interface AuditClientOptions {
apiKey?: string;
apiUrl?: string;
service?: string;
env?: string;
clientTz?: string;
clientOffsetMinutes?: number;
}
class AuditClient {
private readonly apiKey: string;
private readonly apiUrl: string;
private readonly service: string;
private readonly env: string;
private readonly clientTz: string;
private readonly clientOffsetMinutes: number;
constructor(opts: AuditClientOptions = {}) {
this.apiKey = opts.apiKey ?? process.env.IMTBL_API_KEY ?? '';
this.apiUrl = opts.apiUrl ?? process.env.IMTBL_URL ?? 'https://api.immutablelog.com';
this.service = opts.service ?? 'node-service';
this.env = opts.env ?? 'production';
this.clientTz = opts.clientTz ?? 'America/Sao_Paulo';
this.clientOffsetMinutes = opts.clientOffsetMinutes ?? -180;
}
wrap<T extends (...args: unknown[]) => unknown>(
fn: T,
eventName?: string,
kind: 'success' | 'info' = 'success',
): T {
const name = eventName ?? `fn.${fn.name}`;
const isAsync = fn.constructor.name === 'AsyncFunction';
const emit = (startedAt: number, err: Error | null) => {
const payload = {
id: crypto.randomUUID(),
kind: err ? 'error' : kind,
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
};
this._send(name, payload, err ? 'error' : kind);
};
if (isAsync) {
return (async (...args: unknown[]) => {
const t = Date.now();
try { return await (fn as (...a: unknown[]) => Promise<unknown>)(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
return ((...args: unknown[]) => {
const t = Date.now();
try { return fn(...args); }
catch (e) { emit(t, e as Error); throw e; }
finally { emit(t, null); }
}) as unknown as T;
}
private _send(eventName: string, payload: object, kind: string) {
const requestId = crypto.randomUUID();
fetch(`${this.apiUrl}/v1/events`, {
method: 'POST',
headers: {
Authorization: `Bearer ${this.apiKey}`,
'Content-Type': 'application/json',
'Idempotency-Key': `${eventName}-${requestId}`,
'Request-Id': requestId,
'X-Client-TZ': this.clientTz,
'X-Client-Offset-Minutes': String(this.clientOffsetMinutes),
},
body: JSON.stringify({
payload: JSON.stringify(payload),
// Todos os valores de meta precisam ser strings / Every meta value must be a string
meta: { type: kind, event_name: eventName, service: this.service, request_id: requestId, env: this.env },
}),
signal: AbortSignal.timeout(5000),
}).catch(() => {});
}
}
// Inicializar uma vez / Initialize once
const audit = new AuditClient({
service: 'payments-service',
env: 'production',
});
// Usar em qualquer funcao / Use on any function
const processPayment = audit.wrap(
async (id: string) => ({ status: 'approved', id }),
'payment.process',
);
const deleteUser = audit.wrap(
async (userId: number) => { /* ... */ },
'user.delete',
'info',
);Decorator TypeScript (experimental)
Se seu projeto usa TypeScript com `experimentalDecorators: true`, você pode usar o decorator `@AuditLog()` diretamente em métodos de classe. Funciona com métodos sync e async.
// tsconfig.json: "experimentalDecorators": true
function AuditLog(eventName?: string, kind: 'success' | 'info' = 'success') {
return function (
_target: object,
propertyKey: string,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const original = descriptor.value as (...args: unknown[]) => unknown;
const name = eventName ?? `method.${propertyKey}`;
const isAsync = original.constructor.name === 'AsyncFunction';
descriptor.value = isAsync
? async function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return await original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
}
: function (this: unknown, ...args: unknown[]) {
const startedAt = Date.now();
let err: Error | null = null;
try {
return original.apply(this, args);
} catch (e) {
err = e as Error;
throw e;
} finally {
emitAudit(name, kind, startedAt, err);
}
};
return descriptor;
};
}
function emitAudit(name: string, kind: string, startedAt: number, err: Error | null) {
const actualKind = err ? 'error' : kind;
sendEvent({
eventName: name,
payload: {
message: err ? `${name} failed: ${err.constructor.name}` : `${name} executed`,
metrics: { latencyMs: Date.now() - startedAt },
...(err && { error: { exception: err.constructor.name, exceptionMessage: err.message } }),
},
kind: actualKind as 'success' | 'error' | 'info',
}).catch(() => {});
}
// Uso em classe / Usage in class
class PaymentService {
@AuditLog('payment.process', 'success')
async processPayment(paymentId: string): Promise<{ status: string }> {
// ... logica de negocio / business logic ...
return { status: 'approved' };
}
@AuditLog('invoice.generate')
async generateInvoice(orderId: string): Promise<void> {
// ...
}
}Esta documentação reflete o comportamento atual da API. Para dúvidas ou integrações avançadas, entre em contato com o time de suporte.
