Skip to main content
The Python SDK gives you two ways to remove sensitive data from traces before it leaves your application:
  • 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: Change, add, or remove any field on records from instrumented spans.
To redact data after Braintrust receives it instead, or to see all redaction options, see Protect sensitive data.

Choose an approach

For sensitive data:
  • Start with a masking function. It covers every record, and if it raises an exception, 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 set_masking_function() with your function. To remove it later, pass None.
  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 raises, 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.
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.
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
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

Requires Python SDK v0.43.0 or later.
A span customizer is an object whose on_span_export() 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_instrument() and wrappers such as wrap_openai() create. Customizers don’t run on spans you create yourself with traced(), start_span(), or logger.log(), so use them alongside the masking function to cover those spans.

Register a customizer

To create a customizer and register it with the SDK:
  1. Subclass SpanCustomizer and override on_span_export(data). The method receives one record of the span’s data as a dict and returns the record to upload, changed as needed. A span can have several records, as described in How customizers run.
  2. Register an instance with the span_customizers argument to auto_instrument(), or with set_span_customizers(), which you can call at any point in your program. Pass instances, such as RedactEmailAddresses(). Passing a class instead raises TypeError.
Registering a list replaces any customizers already installed:
  • To remove all customizers, call set_span_customizers(None) or set_span_customizers([]).
  • Calling auto_instrument() without span_customizers, or with span_customizers=None, leaves the installed customizers unchanged.
This example replaces email addresses in the input, output, and metadata of each LLM span:
app.py
In Braintrust, the LLM span’s input shows Draft a reply to [EMAIL].

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 dict with the merged fields.
This example adds an environment metadata key and a production tag:

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:

How customizers run

  • Instrumented spans only: The SDK runs customizers on spans that integrations create, through auto_instrument() or wrapper functions such as wrap_openai(). It doesn’t run them on spans you create with traced(), start_span(), 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: on_span_export() must be a regular method. set_span_customizers() and auto_instrument() raise TypeError for an async method.
  • 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.
A customizer must not raise an exception. If it does, the span data can be left in an unexpected state. Catch exceptions inside on_span_export() and return a safe result, such as the record with the sensitive field removed, and test customizers against representative data.

Limitations

  • Feedback comments and metadata: When you log 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 log_feedback().
  • One list per process: Each call to set_span_customizers(), and each auto_instrument() call that passes span_customizers, replaces the whole list. Install every customizer your process needs in one call.

Next steps