> ## 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 Ruby SDK traces before they reach Braintrust using span customizers, which run on every span the SDK exports.

<Note>
  Requires Ruby SDK v0.6.0 or later.
</Note>

Span customizers let you change span data inside your application before the Ruby SDK exports it to Braintrust, for example to redact sensitive values such as email addresses or credentials, or to add tags and metadata.

A span customizer is an object with an `on_span_export` method that the SDK calls on each span after the span ends. The SDK runs customizers on every span it exports, including spans from integrations and spans you create with the OpenTelemetry API.

<Note>
  The Ruby SDK has no masking function, so span customizers are how you remove sensitive data before it leaves your application. To redact data after Braintrust receives it instead, see [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
</Note>

## Register a customizer

To create a customizer and register it with the SDK:

1. **Write the class.** Define an `on_span_export(span)` method. It receives each completed span and returns the span to export. In the method:
   * **Read the attributes you want to change.** Integrations store the span's content as JSON strings in `braintrust.input_json` (input), `braintrust.output_json` (output), and `braintrust.metadata` (metadata). Braintrust parses these into the span's fields.
   * **Change the span and return it.** `span.attributes` is a writable hash, so you can replace or delete entries in place. You can also change other fields, such as `span.name`. To leave a span unchanged, return it as is.
2. **Register an instance.** Pass it in the `span_customizers:` array of `Braintrust.init`.

This example replaces email addresses in the input, output, and metadata of every span:

```ruby theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
require "braintrust"
require "json"

class RedactEmails
  EMAIL = /[\w.+-]+@[\w-]+(?:\.[\w-]+)+/
  CONTENT_KEYS = %w[braintrust.input_json braintrust.output_json braintrust.metadata].freeze

  def on_span_export(span)
    CONTENT_KEYS.each do |key|
      value = span.attributes[key]
      span.attributes[key] = value.gsub(EMAIL, "[EMAIL]") if value.is_a?(String)
    end
    span
  end
end

Braintrust.init(
  default_project: "my-project", # Replace with your project name
  span_customizers: [RedactEmails.new]
)

tracer = OpenTelemetry.tracer_provider.tracer("my-app")
tracer.in_span("draft-reply") do |span|
  span.set_attribute("braintrust.input_json", JSON.generate({to: "jane@example.com"}))
end
```

In Braintrust, the `draft-reply` span's input shows `{"to": "[EMAIL]"}`. The customizer redacts spans from [Ruby SDK integrations](/docs/sdks/ruby/sdk-integrations) the same way, as long as you require the integration's gem before you call `Braintrust.init`.

<Warning>
  Don't combine customizers with `require "braintrust/setup"`, `gem "braintrust", require: "braintrust/setup"`, or `braintrust exec`. Each of these calls `Braintrust.init` without customizers, so a later `Braintrust.init(span_customizers: ...)` adds a second export pipeline, and the first one still sends every span unredacted. To use customizers, add `gem "braintrust"` to your Gemfile, require your provider gems, and then call `Braintrust.init(span_customizers: ...)` yourself.
</Warning>

<Tip>
  `Braintrust.init` auto-instruments the provider gems that are already loaded when you call it.
</Tip>

## Add tags and metadata

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

1. **Read the existing values.** `braintrust.metadata` holds the span's whole metadata object as a JSON string, and `braintrust.tags` holds its tags as an array of strings.
2. **Merge in your values.** Add your keys to the parsed metadata and your tags to the existing list. Replacing either attribute drops what was there.
3. **Write the merged values back** to `span.attributes` and return the span.

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

```ruby theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
class AddEnvironment
  def on_span_export(span)
    existing = span.attributes["braintrust.metadata"]
    metadata = existing ? JSON.parse(existing) : {}
    metadata["environment"] = "production"
    tags = Array(span.attributes["braintrust.tags"]) | ["production"]
    span.attributes["braintrust.metadata"] = JSON.generate(metadata)
    span.attributes["braintrust.tags"] = tags
    span
  rescue JSON::ParserError
    span
  end
end
```

If `on_span_export` raises an error, the SDK drops the whole export batch, as described in [How customizers run](#how-customizers-run). Choose the behavior that fits each customizer:

* **Customizers that add data**, like this one: Return the span unchanged when you can't process it. This example does that when the existing metadata isn't valid JSON.
* **Customizers that redact data**: Raise an error when you can't redact a span, so the SDK drops the batch instead of sending unredacted data.

## 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. **List them in `span_customizers:` in the order you want them to run.** Each customizer receives the span that the previous one returned.

This example runs `RedactEmails`, then `AddEnvironment`:

```ruby theme={"theme":{"light":"github-light","dark":"github-dark-dimmed"}}
Braintrust.init(
  default_project: "my-project", # Replace with your project name
  span_customizers: [RedactEmails.new, AddEnvironment.new]
)
```

## How customizers run

* **Every exported span**: The SDK runs customizers on every span it exports, including spans from integrations, evaluations, and spans you create with the OpenTelemetry API. If you set `filter_ai_spans: true` or `BRAINTRUST_OTEL_FILTER_AI_SPANS=true`, the SDK exports only spans whose name or attributes start with `gen_ai.`, `braintrust.`, `llm.`, `ai.`, or `traceloop.`. Customizers don't run on the spans that this filter or your `span_filter_funcs` drop.
* **Once per completed span**: The SDK runs each customizer once per span, after the span ends, on the background export thread. Keep customizers fast, because slow customizers delay exports. Spans that your customizer creates, for example by calling an instrumented client, aren't traced.
* **Identity**: Customizers can change anything except the trace ID, span ID, and parent span ID, which are read-only. To return a different span instead, build it from `span.to_span_data.dup` so it keeps the same IDs, and return that `OpenTelemetry::SDK::Trace::SpanData`.

<Warning>
  Customizers fail closed. The SDK drops the whole export batch, and logs `Span customization failed; batch not sent`, when a customizer:

  * Raises an error
  * Returns `nil` or anything other than the span or an `OpenTelemetry::SDK::Trace::SpanData`, which drops the batch, not just that span
  * Changes a span's trace, span, or parent span ID

  A batch is the group of spans the SDK exports together, so it can include spans from other traces. The logged error names the customizer's class and the error class, but not the error message, which can contain span data. To record failure details, log them inside your customizer.
</Warning>

## Limitations

* **Code only**: You register customizers in code, with `Braintrust.init`. There's no environment variable for them, and `braintrust/setup` and `braintrust exec` can't register them.
* **Objects, not blocks**: Each entry in `span_customizers:` must respond to `on_span_export`. Procs and lambdas don't, so `Braintrust.init` raises an `ArgumentError` for them, and for any other object without the method.
* **No custom exporter**: Customizers run in the SDK's exporter. If you pass both `span_customizers:` and `exporter:` to `Braintrust.init`, it raises an `ArgumentError`.
* **Fixed at initialization**: `Braintrust.init` copies the `span_customizers:` array, so later changes to the array don't apply.
* **Console output**: Spans that `BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG` prints aren't customized.

## Next steps

* See all redaction options, including ingestion redaction, in [Protect sensitive data](/docs/admin/data-management/protect-sensitive-data).
* See [`span_customizers`](/docs/sdks/ruby/api-reference#span-customizers) in the API reference.
* Set up [Ruby SDK integrations](/docs/sdks/ruby/sdk-integrations) for your AI libraries.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.