Skip to main content
beforeSend is the last thing that runs before an event enters the batch queue. It sees the event after capture gating and redaction, so by the time it fires the shape is final.
createBoundaryLogger({
  beforeSend(event) {
    // Drop events from internal contracts
    if (event.contractName.startsWith("internal-")) return null;

    // Enrich with a trace ID from your request context
    return { ...event, traceId: currentTraceId() } as typeof event;
  },
});

Signature

type BeforeSendHook = (event: BoundaryLogEvent) => BoundaryLogEvent | null;
  • Return the event (unchanged or mutated) to keep it.
  • Return null to drop it entirely — it never hits the queue, never hits the network.
  • Throw — the exception is routed through onError; the event is dropped. A bad hook can never break the contract flow.

beforeSend vs redact.custom

Both give you a final say, but at different granularity:
redact.custombeforeSend
Called perevery leaf valueevery event
Seesone value + its JSON paththe whole event object
Can dropone field (return undefined)the whole event (return null)
Good forscrubbing PII in placecorrelation, routing, filtering
Use them together. Redaction scrubs values; beforeSend makes event-level decisions.

Recipes

Drop noisy contracts

beforeSend(event) {
  if (event.contractName === "health-check") return null;
  return event;
}

Enrich with a trace ID

import { context, trace } from "@opentelemetry/api";

beforeSend(event) {
  const span = trace.getSpan(context.active());
  if (!span) return event;
  const { traceId, spanId } = span.spanContext();
  return { ...event, traceId, spanId } as typeof event;
}

Hash the raw output before it leaves

import { createHash } from "node:crypto";

beforeSend(event) {
  if (typeof event.output !== "string") return event;
  return {
    ...event,
    output: createHash("sha256").update(event.output).digest("hex"),
  } as typeof event;
}
Hashing gives you correlation without exposing content — two runs that produced the same completion are visible as duplicates, but neither the prompt nor the answer leaves your process.

Rate-limit events per contract

const buckets = new Map<string, { count: number; resetAt: number }>();

beforeSend(event) {
  const now = Date.now();
  const key = event.contractName;
  const bucket = buckets.get(key);

  if (!bucket || bucket.resetAt < now) {
    buckets.set(key, { count: 1, resetAt: now + 60_000 });
    return event;
  }

  bucket.count += 1;
  if (bucket.count > 1000) return null;  // drop — over local budget
  return event;
}

Errors inside beforeSend

If your hook throws, the event is dropped and onError fires with the thrown value. The contract that produced the event is unaffected — the hook runs on a separate async path.
createBoundaryLogger({
  beforeSend(event) {
    throw new Error("boom");
  },
  onError(err) {
    console.warn("Boundary drop:", err);
  },
});

See also

Redaction

Per-leaf scrubbing

Capture policy

Strip whole buckets before beforeSend sees the event