# Mask Sensitive Trace Data

Protect your sensitive information from traces using the masking feature

## Overview

Masking allows you to automatically redact or transform sensitive data in your traces before they're sent to the observatory. Masking is essential for several reasons:

- **Security**: Prevent exposure of credentials or sensitive business data
- **Regulatory Compliance**: Meet requirements like GDPR, HIPAA, or CCPA

By default, [`confident-trace`](https://github.com/confident-ai/confident-trace) captures the full content of your spans — prompts, completions, tool arguments, retrieved chunks, and anything you set through the update helpers. If that content can contain PII, you have four controls, all configured once in `init()`:

| Control         | Python `init()`     | TypeScript `init()` | What it does                                                              |
| --------------- | ------------------- | ------------------- | ------------------------------------------------------------------------- |
| Disable capture | `capture_content`   | `captureContent`    | Turn content capture off entirely — keep timing, status, model, and usage |
| Redact          | `redact`            | `redact`            | Run your own masking function over every content value before export      |
| Size limit      | `max_content_bytes` | `maxContentBytes`   | Optionally cap the size of each content attribute (disabled by default)   |
| Depth limit     | `max_nesting_depth` | `maxNestingDepth`   | Cap how many levels of nesting each content value keeps (12 by default)   |

> These controls apply to content that `confident-trace` itself manages — spans from its integrations and anything you set with `update_span()` / `update_trace()`. Native framework instrumentation and third-party OpenTelemetry instrumentors keep their own content policies, so check those separately if you use them.

## Configure Masking

To implement masking, define a masking function and pass it to `init()` as `redact`. It runs over every content value right before serialization, so nothing sensitive ever leaves your process.

#### Python

```python title="main.py" {13}
import re
from confident_trace import init, span, shutdown

def masking_function(data):
    if isinstance(data, str):
        return re.sub(r'\b(?:\d{4}[- ]?){3}\d{4}\b', '[REDACTED CARD]', data)
    if isinstance(data, list):
        return [masking_function(item) for item in data]
    if isinstance(data, dict):
        return {k: masking_function(v) for k, v in data.items()}
    return data

init(redact=masking_function)

@span(type="agent")
def llm_app(query: str):
    return "4242-4242-4242-4242"

try:
    llm_app("Test Masking")
finally:
    shutdown()
```

#### TypeScript

```typescript title="src/index.ts" {13} maxLines={0}
import { init, span } from "confident-trace";

const maskingFunction = (data: unknown): unknown => {
  if (typeof data === "string")
    return data.replace(/\b(?:\d{4}[- ]?){3}\d{4}\b/g, "[REDACTED CARD]");
  if (Array.isArray(data)) return data.map(maskingFunction);
  if (data && typeof data === "object") {
    return Object.fromEntries(Object.entries(data).map(([k, v]) => [k, maskingFunction(v)]));
  }
  return data;
};

const runtime = init({ redact: maskingFunction });

const llmApp = span({ name: "llm_app", type: "agent" }, (query: string) => {
  return "4242-4242-4242-4242";
});

try {
  llmApp("Test Masking");
} finally {
  await runtime.shutdown();
}
```

Remember to launch your entry point with the [Node preload](/docs/llm-tracing/quickstart#instrument-your-ai-app) so integration spans are masked too.

The masking function is automatically applied to:

1. **Span I/O**: the captured input and output of every span — function arguments and return values, as well as the messages and completions recorded by integrations
2. **Update helper fields**: anything you set through `update_span()` / `update_trace()`, such as `input`, `output`, `retrieval_context`, and `metadata`

> Since the masking function is applied to inputs, outputs, and helper fields alike, it must handle the various data types it might receive — strings, lists, dicts, and everything else — and return them in the same shape. The examples on this page recurse through lists and dicts and pass any other type through untouched.

## Disable Content Capture

If you'd rather not export prompts and completions at all — for example in a regulated environment where masking isn't enough — turn content capture off. You still get timing, status, span hierarchy, and the model and token usage attributes, so cost tracking and latency monitoring keep working; only the content fields are omitted.

#### Python

```python title="main.py" {3}
from confident_trace import init

init(capture_content=False)
```

#### TypeScript

```typescript title="src/index.ts" {3}
import { init } from "confident-trace";

const runtime = init({ captureContent: false });
```

> You can also disable capture for a single span with `capture_content=False` / `captureContent: false` on that span's options, which is handy when only one step of your pipeline touches sensitive data.

> Content controls only govern the content that `confident-trace` manages. Raw OpenTelemetry attributes you attach yourself bypass them entirely, so don't put sensitive values there. For a complete opt-out, prefer disabling capture over trying to redact everything.

## Size Limits

Content size limits are disabled by default. Unless you configure one,
`confident-trace` does not cap content attributes.

Set `max_content_bytes` / `maxContentBytes` to enable a limit. Values above the
limit are truncated or omitted, and chat message arrays keep a valid, bounded
prefix where possible so the span still renders. Streamed outputs are bounded
the same way and marked as truncated, while token usage keeps being tracked:

#### Python

```python title="main.py"
from confident_trace import init

init(max_content_bytes=8192)
```

#### TypeScript

```typescript title="src/index.ts"
import { init } from "confident-trace";

init({ maxContentBytes: 8192 });
```

> These limits apply to what's exported, not to buffers your application or framework keeps in memory. If you're using the [Vercel AI SDK](/docs/integrations/third-party/vercel-ai-sdk) in automatic mode, `captureContent: false` also sets its `recordInputs` and `recordOutputs` to `false` for you; in manual mode, disable both flags yourself.

## Depth Limit

Each content value keeps up to 12 levels of nested lists and objects by
default. Anything deeper is replaced with `[truncated]`, so a span still
renders when a framework hands over deeply nested state, such as tool call
arguments inside a LangGraph command.

Set `max_nesting_depth` / `maxNestingDepth` to any whole number from `1` to
`64` to change the limit, or set the `CONFIDENT_MAX_NESTING_DEPTH` environment
variable. Explicit arguments win over the environment variable, and an invalid
environment value falls back to `12`:

#### Python

```python title="main.py"
from confident_trace import init

init(max_nesting_depth=16)
```

#### TypeScript

```typescript title="src/index.ts"
import { init } from "confident-trace";

init({ maxNestingDepth: 16 });
```

> Only raise the limit if values you need show up as `[truncated]`. The size
> limit still applies, so a deeper limit never makes a content attribute larger
> than `max_content_bytes` / `maxContentBytes` allows.

## Next Steps

With sensitive data masked, control which traces are sent in the first place.

#### [Sample Traces](/docs/llm-tracing/features/sampling)

Send only a percentage of traces to Confident AI to keep volume and cost
under control in high-traffic apps.

#### [Drop Traces](/docs/llm-tracing/features/dropping-traces)

Skip tracing entirely for health checks, internal test requests, and other
noise you don't want in the Observatory.
