- 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 any SDK span.
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 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
tagsorerror, adding metadata, or deciding what to change based on other fields in the record.
Masking functions
A masking function is a function you write that replaces sensitive parts of logged values, using rules such as matching field names likepassword 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:
- 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.
- Install it once, at startup. Call
setMaskingFunction()with your function. To remove it later, passnull. - 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.
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, andcontext, it replaces the value with an error-message string. - For
metadata, it replaces the value with an object containing anerrormessage. - For
scoresandmetrics, it removes the field and adds a diagnostic to the record’serrorfield.
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, andtoken - Text in strings that follows
api_key,password, ortoken, such aspassword: <password>
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.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
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 TypeScript SDK v3.35.0 or later. Customizing OpenTelemetry spans requires
@braintrust/otel v1.1.0 or later with TypeScript SDK v3.38.0 or later.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 all native SDK spans, including spans you create with traced(), startSpan(), and logger.log(), as well as spans that auto-instrumentation and wrappers such as wrapOpenAI() create. Customizers don’t run on dataset rows or feedback. In v3.35.0, customizers run only on spans from auto-instrumentation and wrappers. With @braintrust/otel v1.1.0 or later, customizers also run on OpenTelemetry spans that BraintrustSpanProcessor or BraintrustExporter sends to Braintrust.
Register a customizer
To create a customizer and register it with the SDK:- 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. - Import
configureInstrumentation()frombraintrust/instrumentation. The same function is also exported frombraintrust, but importingbraintrustturns instrumentation on, so a call through that import logs a warning and has no effect. - Call it with your customizers at startup, before you import
braintrustor any AI SDK. - Load
braintrustand your AI SDKs with dynamicimport()calls. Static imports are hoisted above theconfigureInstrumentation()call.
output_text on OpenAI Responses API spans:
app.ts
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:- Read the record’s existing
metadataandtags, if the record has them. - Merge in your values. Replacing
metadataortagsdrops the values the integration put in that record. - Return the record, or a new plain object with the merged fields.
environment metadata key and a production tag:
Chain customizers
To run more than one customizer:- Write each customizer separately, so each one handles one change and you can test and reuse it on its own.
- Pass them in the order you want them to run. Each customizer receives the record that the previous one returned.
redactEmailAddresses, then addEnvironment:
How customizers run
- All native SDK spans: The SDK runs customizers on all native SDK spans, including spans you create with
traced(),startSpan(), andlogger.log(), as well as spans from auto-instrumentation and wrappers. Customizers don’t run on 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 the record directly. If it returns a promise, the SDK treats the call as a failure. - OpenTelemetry compatibility mode: Customizers run when you enable compatibility mode with
BRAINTRUST_OTEL_COMPATorsetupOtelCompat(), with@braintrust/otelv1.1.0 or later and SDK v3.38.0 or later. Earlier versions don’t apply customizers in compatibility mode. - 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.
Customizer failures
If a customizer throws an error or returns an invalid value, the SDK skips the remaining customizers and keeps that record’s data out of the upload. In v3.38.0 or later, the SDK uploads the record with only its protected fields, itsmetrics.start and metrics.end timestamps when present, and context set to { customizer_error: true }, so the span keeps its place in the trace. The masking function doesn’t run on that record, and its attachments aren’t uploaded. Unless debugLogLevel is false, the SDK logs an error the first time a customizer fails in a process, without the error details or the record’s data.
OpenTelemetry spans
With@braintrust/otel v1.1.0 or later and TypeScript SDK v3.38.0 or later, the same customizers also run on spans that BraintrustSpanProcessor or BraintrustExporter sends to Braintrust, with or without compatibility mode. Generic OTLPTraceExporter instances don’t run them.
For these spans:
- Data:
onSpanExport()receives a detached copy of the finished span as a plain object with keys such asname,kind,attributes,events,links,status,resource, andinstrumentationScope. Span data such as inputs and outputs is inattributes.kindandstatus.codeare integer values. Attribute values must be strings, numbers, booleans, or arrays of them. - Once per span: Each customizer runs once per finished span, at export time, after
filterAISpansorcustomFilterfiltering. The SDK waits for asynchronous resource attributes first, so customizers can also change those. - Protected fields: The SDK restores
spanContext,parentSpanId,parentSpanContext,startTime,endTime,duration, and thebraintrust.parentattribute after every call. - Failures: If a customizer throws, or returns values that OpenTelemetry can’t export, the SDK logs an error once per process and exports only the span’s identity, timing, and
braintrust.parentattribute, with thebraintrust.context_jsonattribute set to{"customizer_error":true}. Other spans in the batch are exported normally. If the SDK can’t export even that, it drops the span.
Limitations
- Feedback comments and metadata: When you log user feedback, the masking function runs on its
scoresandexpectedvalues but not on itscommentormetadata. If users can type sensitive data into feedback, remove it before you calllogFeedback(). - Import path:
configureInstrumentation()imported frombraintrusthas no effect. Import it frombraintrust/instrumentation. - Evaluation cache: In 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: falsein theEval()options.
Next steps
- See all redaction options, including ingestion redaction, in Protect sensitive data.
- See
setMaskingFunction(),configureInstrumentation(), andSpanCustomizerin the API reference. - Set up auto-instrumentation for your AI SDKs.