> ## Documentation Index
> Fetch the complete documentation index at: https://docs.withboundary.com/llms.txt
> Use this file to discover all available pages before exploring further.

# beforeSend

> Per-event transform and drop hook

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

```typescript theme={null}
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

```typescript theme={null}
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.custom`                | `beforeSend`                    |
| ---------- | ------------------------------ | ------------------------------- |
| Called per | every leaf value               | every event                     |
| Sees       | one value + its JSON path      | the whole event object          |
| Can drop   | one field (return `undefined`) | the whole event (return `null`) |
| Good for   | scrubbing PII in place         | correlation, routing, filtering |

Use them together. Redaction scrubs values; `beforeSend` makes event-level decisions.

## Recipes

### Drop noisy contracts

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

### Enrich with a trace ID

```typescript theme={null}
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

```typescript theme={null}
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

```typescript theme={null}
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.

```typescript theme={null}
createBoundaryLogger({
  beforeSend(event) {
    throw new Error("boom");
  },
  onError(err) {
    console.warn("Boundary drop:", err);
  },
});
```

## See also

<CardGroup cols={2}>
  <Card title="Redaction" icon="eye-slash" href="/sdk/redaction">
    Per-leaf scrubbing
  </Card>

  <Card title="Capture policy" icon="filter" href="/sdk/capture-policy">
    Strip whole buckets before beforeSend sees the event
  </Card>
</CardGroup>
