Skip to main content
import { createBoundaryLogger } from "@withboundary/sdk";

Signature

function createBoundaryLogger<T = unknown>(
  options?: BoundaryLoggerOptions,
): BoundaryLogger<T> | null;

Returns

  • A BoundaryLogger<T> when either apiKey (or BOUNDARY_API_KEY env var) or write is configured.
  • null otherwise — the dev-safe fallback. Passing null to defineContract({ logger }) is a no-op.

BoundaryLoggerOptions

OptionTypeDefaultDescription
apiKeystringprocess.env.BOUNDARY_API_KEYBoundary ingest credential
environment"production" | "staging" | "development"Bucket events on the dashboard
endpointstring"https://api.withboundary.com"Override the ingest endpoint (self-host, proxy)
modelstringDefault LLM label stamped onto every event; overridable per-call
capturePartial<CapturePolicy>see belowWhich buckets of data to ship
redactRedactionOptionsfields / patterns / custom scrubbing
batch.sizenumber20Flush when queue hits this length. Small enough that low-traffic apps don’t wait long for the first flush; large enough that bursts coalesce into one POST.
batch.intervalMsnumber5000Periodic flush cadence. Caps the worst-case time between an event firing and showing up on the dashboard. Set to 0 to disable the timer (recommended on Cloudflare Workers / Vercel Edge — the isolate freezes between requests, so timers are unreliable).
batch.maxQueueSizenumber1000Drop-oldest overflow threshold. Bounds memory during a backend outage. Tighten to ~100 on serverless/edge runtimes that recycle frequently.
beforeSend(event) => BoundaryLogEvent | nullLast-chance transform or drop
write(events) => void | Promise<void>Custom sink; fires alongside apiKey
flushOnExitbooleantrueAttach runtime lifecycle drain hooks
onError(err) => voidone-time console.warnPermanent drop callback
fetchtypeof fetchglobalThis.fetchInjected fetch (tests, polyfills)

Default capture policy

{
  inputs: false,
  outputs: false,
  repairs: true,
}
Three optional buckets. Raw LLM inputs and outputs are off by default; repair messages are on so the dashboard can show how the model recovered. Failure attribution (category, issues, ruleFailures) and run metadata are always sent — they aren’t gated. See Capture policy.

Tuning by platform

RuntimeRecommended overridesWhy
Long-running NodeDefaultsThe 5s timer drains continuously; 1000-event queue absorbs short outages.
AWS Lambda / Vercel Functionsbatch.maxQueueSize: 100, optionally tighter batch.sizeContainers freeze between invocations; flushes happen via flush() in your handler’s finally block, not the timer.
Cloudflare Workers / Vercel Edgebatch.intervalMs: 0, batch.maxQueueSize: 100, drain via ctx.waitUntil(logger.flush())Timers are unreliable when the isolate freezes; explicit drain on the request lifecycle is the only reliable path.
Browserbatch.size: 10, batch.intervalMs: 2000, batch.maxQueueSize: 100, write to your authenticated proxyBrowsers die more often than servers; smaller, faster batches, never an apiKey in the bundle.
See Node, Vercel & Lambda, Cloudflare Workers & Edge, and Browser for full per-runtime patterns.

BoundaryLogger<T>

The returned object:
type BoundaryLogger<T = unknown> = ContractLogger<T> & {
  flush: (timeoutMs?: number) => Promise<void>;
  shutdown: (timeoutMs?: number) => Promise<void>;
};
  • Implements every ContractLogger<T> hook (assign it directly to defineContract({ logger })).
  • flush(timeoutMs?) — drain the queue; logger stays active.
  • shutdown(timeoutMs?) — drain, stop the timer, disable further sends. Idempotent.
See Shutdown for timeout semantics and per-platform recipes.

Minimal example

import { createBoundaryLogger } from "@withboundary/sdk";
import { defineContract } from "@withboundary/contract";

const logger = createBoundaryLogger({
  environment: "production",
});

const contract = defineContract({
  name: "lead-scoring",
  schema,
  rules,
  logger,
});

Full example

const logger = createBoundaryLogger({
  apiKey: process.env.BOUNDARY_API_KEY,
  environment: "production",
  model: "gpt-4.1",
  capture: {
    inputs: false,
    outputs: false,
    repairs: true,
  },
  redact: {
    fields: ["ssn", "email"],
    patterns: [/\b\d{3}-\d{2}-\d{4}\b/],
    custom: (value, path) =>
      path.at(-1) === "customerId" && typeof value === "string"
        ? `cust_${hash(value).slice(0, 10)}`
        : value,
  },
  batch: {
    size: 50,
    intervalMs: 2000,
    maxQueueSize: 2000,
  },
  beforeSend(event) {
    return event.contractName === "health-check" ? null : event;
  },
  write(events) {
    for (const e of events) console.log(JSON.stringify(e));
  },
  onError(err) {
    metrics.increment("boundary.drop");
  },
});

See also

SDK Quickstart

Install and wire the logger

Shutdown

flush + shutdown + signal handling