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

# Redact sensitive data

> Remove sensitive data from TypeScript SDK traces before upload with a masking function, span customizers, or both.

The TypeScript SDK gives you two ways to remove sensitive data from traces before it leaves your application:

* **[Masking functions](#masking-functions)**: Replace sensitive parts of a fixed set of field values, on every record the SDK logs, including spans, dataset rows, and feedback scores.
* **[Span customizers](#span-customizers)**: Change, add, or remove any field on records from instrumented spans.

<Note>
  To redact data after Braintrust receives it instead, or to see all redaction options, see [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
</Note>

## Choose an approach

| | Masking function | Span customizer |
| - | - | - |
| Receives | One field value at a time | The whole record |
| Can change | `input`, `output`, `expected`, `metadata`, `context`, `scores`, and `metrics` | Any field except [protected fields](#how-customizers-run) |
| Covers | Every record, including spans you create, dataset rows, and feedback scores | Spans from instrumentation and wrappers only |
| If it throws | The SDK uploads an error message instead of the field's value | Undefined, so customizers must not throw |

For sensitive data:

* **Start with a masking function.** It covers every record, and if it throws an error, the SDK uploads an error message in place of the affected field instead of the unmasked value.
* **Add span customizers** for changes masking can't make, such as editing `tags` or `error`, adding metadata, or deciding what to change based on other fields in the record.

When you use both, customizers run first, on each record. Masking then runs just before upload, after the SDK combines each span's records, so it also sees your customizers' output.

## Masking functions

A masking function is a function you write that replaces sensitive parts of logged values, using rules such as matching field names like `password` or text patterns like email addresses. The SDK applies it just before upload to every record it logs, including spans, dataset rows, and feedback scores.

To mask sensitive data with a masking function:

1. **Write the function.** It receives one logged value and returns the value to upload, with sensitive parts replaced. Values can be strings, objects, or arrays. The function can modify values in place to reduce copying.
2. **Install it once, at startup.** Call [`setMaskingFunction()`](/docs/sdks/typescript/api-reference#setmaskingfunction) with your function. To remove it later, pass `null`.
3. **Test it against representative data.** Check that sensitive values are masked and the rest of the trace stays intact. Use the examples below as starting points.

The SDK passes the value of each present field to the function separately: `input`, `output`, `expected`, `metadata`, `context`, `scores`, and `metrics`. It doesn't pass `error` or `tags`, and it doesn't say which field a value came from.

If the function throws, the SDK keeps the affected field's value out of the upload:

* For `input`, `output`, `expected`, and `context`, it replaces the value with an error-message string.
* For `metadata`, it replaces the value with an object containing an `error` message.
* For `scores` and `metrics`, it removes the field and adds a diagnostic to the record's `error` field.

### Mask credentials

This example finds credentials by name and replaces them with `[REDACTED]`, leaving the rest of the trace data as is. It replaces:

* Values of fields named like a credential, such as `api_key`, `password`, and `token`
* Text in strings that follows `api_key`, `password`, or `token`, such as `password: <password>`

It walks nested objects and arrays, so the same rules apply throughout the logged fields.

```typescript expandable theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { setMaskingFunction, initLogger } from "braintrust";

// Mask API keys, passwords, and tokens
const maskFunction = (data: unknown): unknown => {
  if (typeof data === "string") {
    return data.replace(
      /\b(api[_-]?key|password|token)[\s:=]+\S+/gi,
      "$1: [REDACTED]",
    );
  }

  if (typeof data === "object" && data !== null) {
    if (Array.isArray(data)) {
      return data.map((item) => maskFunction(item));
    }

    const masked: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      if (/^(api[_-]?key|password|secret|token|auth|credential)$/i.test(key)) {
        masked[key] = "[REDACTED]";
      } else {
        masked[key] = maskFunction(value);
      }
    }
    return masked;
  }

  return data;
};

setMaskingFunction(maskFunction);

const logger = initLogger({ projectName: "My Project" });

logger.log({
  input: { query: "Process payment", api_key: "your-api-key" },
  metadata: { password: "super-secret" },
});
```

The example replaces both `input.api_key` and `metadata.password` with `[REDACTED]` and keeps the query.

### Mask PII

When you know which kinds of personally identifiable information (PII) your application records, match them by field name and regular expression. This example masks email addresses, US phone numbers, and social security numbers in text and nested fields. The patterns don't detect every form of PII, such as person names, so adapt and test them for your data.

```typescript expandable wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { setMaskingFunction, initLogger } from "braintrust";

const maskPII = (data: unknown): unknown => {
  if (typeof data === "string") {
    let masked = data;
    // Mask email addresses
    masked = masked.replace(
      /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
      "[EMAIL]",
    );
    // Mask phone numbers (US format)
    masked = masked.replace(/\b\d{3}[-.]?\d{3}[-.]?\d{4}\b/g, "[PHONE]");
    // Mask SSN
    masked = masked.replace(/\b\d{3}-\d{2}-\d{4}\b/g, "[SSN]");
    return masked;
  }

  if (typeof data === "object" && data !== null) {
    if (Array.isArray(data)) {
      return data.map((item) => maskPII(item));
    }

    const masked: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      if (
        ["email", "phone", "ssn", "phone_number"].includes(key.toLowerCase())
      ) {
        masked[key] = `[${key.toUpperCase()}]`;
      } else {
        masked[key] = maskPII(value);
      }
    }
    return masked;
  }

  return data;
};

setMaskingFunction(maskPII);

// Usage example
const logger = initLogger({ projectName: "My Project" });

logger.log({
  input: {
    message: "Contact john.doe@example.com or call 555-123-4567",
    user: {
      name: "John Doe",
      email: "john.doe@example.com",
      phone: "555-123-4567",
      ssn: "123-45-6789",
    },
  },
});
```

The example replaces the email address, phone number, and social security number with category markers, and leaves the name `John Doe` unchanged.

### Apply custom rules

To keep useful context alongside masked values, write rules that understand the structure of your data. This example:

* Keeps the last four card digits and the amount in a transaction, and replaces its CVV with `XXX`
* Removes confidential text
* Keeps identifiers and timestamps

```typescript expandable wrap theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { setMaskingFunction, initLogger } from "braintrust";

const customMask = (data: unknown): unknown => {
  // Handle different data types
  if (typeof data === "string") {
    // Only mask if the string contains certain keywords
    if (data.toLowerCase().includes("confidential")) {
      return "[CONFIDENTIAL DATA REMOVED]";
    }
    return data;
  }

  if (typeof data === "object" && data !== null) {
    // Handle special data structures
    if ("credit_card" in data && "cvv" in data) {
      // Mask credit card info but keep last 4 digits
      return {
        ...data,
        credit_card:
          typeof data.credit_card === "string"
            ? data.credit_card.replace(/\d(?=(?:\D*\d){4})/g, "X")
            : data.credit_card,
        cvv: "XXX",
      };
    }

    if (Array.isArray(data)) {
      return data.map((item) => customMask(item));
    }

    // Default object handling
    const masked: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      // Skip masking for specific fields
      if (["timestamp", "request_id", "trace_id"].includes(key)) {
        masked[key] = value;
      } else {
        masked[key] = customMask(value);
      }
    }
    return masked;
  }

  return data;
};

setMaskingFunction(customMask);

// Usage example
const logger = initLogger({ projectName: "My Project" });

logger.log({
  input: {
    transaction: {
      credit_card: "4532-1234-5678-9012",
      cvv: "123",
      amount: 15000,
      timestamp: "2024-01-01T00:00:00Z",
    },
    internal_note: "Confidential: Premium customer",
  },
  metadata: {
    trace_id: "trace-123",
    debug_info: "Processing large transaction",
  },
});
```

The example keeps the card suffix `9012`, the transaction amount `15000`, and the timestamp. It replaces the CVV with `XXX` and the internal note with `[CONFIDENTIAL DATA REMOVED]`, and leaves the metadata unchanged.

## Span customizers

<Note>
  Requires TypeScript SDK v3.35.0 or later.
</Note>

A span customizer is an object whose `onSpanExport()` method changes span data before the SDK uploads it. Use customizers to redact values, add tags or metadata, or remove fields, including `tags` and `error`.

The SDK calls customizers on spans that [auto-instrumentation](/docs/instrument/trace-llm-calls) and wrappers such as `wrapOpenAI()` create. Customizers don't run on spans you create yourself with `traced()`, `startSpan()`, or `logger.log()`, so use them alongside the [masking function](#masking-functions) to cover those spans.

### Register a customizer

To create a customizer and register it with the SDK:

1. **Write the customizer.** Create an object with an `onSpanExport(data)` method. It receives one record of the span's data as a plain object and returns the record to upload, changed as needed. A span can have several records, as described in [How customizers run](#how-customizers-run).
2. **Import [`configureInstrumentation()`](/docs/sdks/typescript/api-reference#configureinstrumentation) from `braintrust/instrumentation`.** The same function is also exported from `braintrust`, but importing `braintrust` turns instrumentation on, so a call through that import logs a warning and has no effect.
3. **Call it with your customizers at startup**, before you import `braintrust` or any AI SDK.
4. **Load `braintrust` and your AI SDKs with dynamic `import()` calls.** Static imports are hoisted above the `configureInstrumentation()` call.

This example replaces email addresses in the input, output, and metadata of each LLM span. It includes metadata because some integrations copy response text into it, such as `output_text` on OpenAI Responses API spans:

```typescript app.ts expandable theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import {
  configureInstrumentation,
  type SpanCustomizer,
} from "braintrust/instrumentation";

const EMAIL = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/g;

function redactEmails(value: unknown): unknown {
  if (typeof value === "string") {
    return value.replace(EMAIL, "[EMAIL]");
  }
  if (Array.isArray(value)) {
    return value.map(redactEmails);
  }
  // Only walk plain objects, so SDK values such as attachments pass through
  if (
    typeof value === "object" &&
    value !== null &&
    Object.getPrototypeOf(value) === Object.prototype
  ) {
    return Object.fromEntries(
      Object.entries(value).map(([key, item]) => [key, redactEmails(item)]),
    );
  }
  return value;
}

const redactEmailAddresses: SpanCustomizer = {
  onSpanExport(data) {
    for (const field of ["input", "output", "metadata"]) {
      if (field in data) {
        data[field] = redactEmails(data[field]);
      }
    }
    return data;
  },
};

configureInstrumentation({ spanCustomizers: [redactEmailAddresses] });

const { initLogger } = await import("braintrust");
const { default: OpenAI } = await import("openai");

const logger = initLogger({ projectName: "my-project" }); // Replace with your project name
const client = new OpenAI();

await client.responses.create({
  model: "gpt-5-mini",
  input: "Draft a reply to jane@example.com.",
});

await logger.flush();
```

Run the app with auto-instrumentation enabled:

```bash theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
node --import braintrust/hook.mjs app.ts
```

Running `app.ts` directly requires Node.js 22.18 or later, which strips TypeScript types at runtime. On earlier versions, compile the example to JavaScript and run the `.js` file.

In Braintrust, the LLM span's input shows `Draft a reply to [EMAIL].` The customizer also replaces any email address in the response, in both the output and `metadata.output_text`.

### Add tags and metadata

To add metadata or tags to a span without losing the values the integration recorded:

1. **Read the record's existing `metadata` and `tags`**, if the record has them.
2. **Merge in your values.** Replacing `metadata` or `tags` drops the values the integration put in that record.
3. **Return the record**, or a new plain object with the merged fields.

This example adds an `environment` metadata key and a `production` tag:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
import { type SpanCustomizer } from "braintrust/instrumentation";

function isRecord(value: unknown): value is Record<string, unknown> {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

const addEnvironment: SpanCustomizer = {
  onSpanExport(data) {
    const metadata = isRecord(data.metadata) ? data.metadata : {};
    const tags = Array.isArray(data.tags) ? data.tags : [];
    return {
      ...data,
      metadata: { ...metadata, environment: "production" },
      tags: tags.includes("production") ? tags : [...tags, "production"],
    };
  },
};
```

### Chain customizers

To run more than one customizer:

1. **Write each customizer separately**, so each one handles one change and you can test and reuse it on its own.
2. **Pass them in the order you want them to run.** Each customizer receives the record that the previous one returned.

This example runs `redactEmailAddresses`, then `addEnvironment`:

```typescript theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
configureInstrumentation({
  spanCustomizers: [redactEmailAddresses, addEnvironment],
});
```

### How customizers run

* **Instrumented spans only**: The SDK runs customizers on spans that auto-instrumentation and wrapper functions create. It doesn't run them on spans you create with `traced()`, `startSpan()`, or `logger.log()`, dataset rows, or feedback.
* **Several calls per span**: The SDK sends each span's data in several records as the data becomes available, such as one record with the input when an LLM call starts and another with the output when it finishes. Braintrust combines the records into one span. Your customizer runs once on each record, so:
  * A field can be missing from any one call. Check that a field is present before you change it.
  * A call can't see the span's other records. For example, it can't change the output based on the input.
* **Synchronous**: `onSpanExport()` must return a value directly. The SDK doesn't await a returned promise.
* **Protected fields**: The SDK restores these fields after every call, so changing them has no effect: `id`, `span_id`, `root_span_id`, `span_parents`, `org_id`, `project_id`, `experiment_id`, `dataset_id`, `prompt_session_id`, `log_id`, `function_data`, `_is_merge`, `_merge_paths`, `_parent_id`, `_object_delete`, `_array_delete`, and `_xact_id`.

<Warning>
  A customizer must not throw an error. If it does, the span data can be left in an unexpected state. Catch errors inside `onSpanExport()` and return a safe result, such as the record with the sensitive field removed, and test customizers against representative data.
</Warning>

## Limitations

* **Feedback comments and metadata**: When you [log user feedback](/docs/instrument/user-feedback), the masking function runs on its `scores` and `expected` values but not on its `comment` or `metadata`. If users can type sensitive data into feedback, remove it before you call `logFeedback()`.
* **Import path**: `configureInstrumentation()` imported from `braintrust` has no effect. Import it from `braintrust/instrumentation`.
* **Evaluation cache**: In [evaluations](/docs/evaluate/run-evaluations), the SDK caches span data on local disk for scorers before customizers and masking run. To keep unredacted values off local disk, pass `enableCache: false` in the `Eval()` options.

## Next steps

* See all redaction options, including ingestion redaction, in [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
* See [`setMaskingFunction()`](/docs/sdks/typescript/api-reference#setmaskingfunction), [`configureInstrumentation()`](/docs/sdks/typescript/api-reference#configureinstrumentation), and [`SpanCustomizer`](/docs/sdks/typescript/api-reference#spancustomizer) in the API reference.
* Set up [auto-instrumentation](/docs/instrument/trace-llm-calls) for your AI SDKs.
