Pular para o conteúdo principal
ImmutableLog logo
VoltarNode.js / TypeScript

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.

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.

typescript
// 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';

export interface SendEventOptions {
  eventName: string;
  payload: Record<string, unknown>;
  kind?: 'success' | 'error' | 'info' | 'warning';
  service?: string;
  env?: string;
}

export async function sendEvent(opts: SendEventOptions): Promise<{ tx_id: string; payload_hash: string }> {
  const { eventName, payload, kind = 'info', service = 'node-service', env = 'production' } = opts;
  const requestId = crypto.randomUUID();
  const payloadStr = JSON.stringify({
    ...payload,
    timestamp: new Date().toISOString(),
  });

  const event = {
    payload: payloadStr,
    meta: {
      type: kind,
      event_name: eventName,
      service,
      request_id: requestId,
      env,
    },
  };

  const res = await fetch(`${IMTBL_URL}/v1/events`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${IMTBL_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `${eventName}-${requestId}`,
      'Request-Id': requestId,
    },
    body: JSON.stringify(event),
    signal: AbortSignal.timeout(5000),
  });

  if (!res.ok) throw new Error(`ImmutableLog error: ${res.status}`);
  return res.json() as Promise<{ tx_id: string; payload_hash: string }>;
}

// Uso / Usage
await sendEvent({
  eventName: 'payment.approved',
  payload: { paymentId: 'pay_abc123', amount: 299.90, currency: 'BRL' },
  kind: 'success',
  service: 'payments-service',
});

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.

typescript
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.

typescript
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.

typescript
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.

typescript
import crypto from 'crypto';

interface AuditClientOptions {
  apiKey?: string;
  apiUrl?: string;
  service?: string;
  env?: string;
}

class AuditClient {
  private readonly apiKey: string;
  private readonly apiUrl: string;
  private readonly service: string;
  private readonly env: string;

  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';
  }

  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,
      },
      body: JSON.stringify({
        payload: JSON.stringify(payload),
        meta: { type: kind, event_name: eventName, service: this.service, 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.

typescript
// 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.