Skip to content

Keep content private

This is the practical page: which switch to throw, where to throw it, and what each choice costs you. For what the two content stages actually are and why storage and transmission are different questions, read Content and privacy first.

Metadata is always captured: timings, token counts, model, route, status, tags, tool-call names and a content-free structural template hash. None of the controls below turn that off. They control prompt and completion text, and tool-call arguments and results.

The blunt instrument, and the right one if no prompt text may leave your process at all.

app/main.py
import metergraph
metergraph.init(capture_text=False)

Or without touching code, which is usually the better answer because it can differ per environment:

.env
METERGRAPH_CAPTURE_TEXT=0

The explicit argument wins over the environment variable. With capture off, the SDK marks each row as opted out, and the ingest API drops the request and response fields before anything is written durably. Tool calls are reduced to their names, ids and statuses.

What it costs you. Analysis replays real captured requests, so a route with no captured content cannot be analysed and cannot be turned into a dataset. The Traces page shows the shape of each call but not what was said. You keep every cost, latency, failure and attribution view.

Most teams do not need an all-or-nothing answer. They need one route to stay dark: the one that handles medical notes, or raw customer records, or anything under a contract that forbids third-party storage. Capture stays on everywhere else.

app/intake.py
with metergraph.route("patient-intake", capture_text=False):
summary = client.chat.completions.create(...)

route() also works as a decorator, and trace() takes the same argument for a whole multi-call operation:

@metergraph.route("patient-intake", capture_text=False)
def summarize(note): ...

The setting is inherited by everything nested inside the scope. A nested scope that sets it again wins, in either direction, so keep the exemption close to the sensitive work and do not set capture_text anywhere below it.

What it costs you. Exactly the same trade, scoped to one route: no replay, no analysis, no dataset for that route. Everything else in the workspace is unaffected.

When you want the shape of the prompt but not the values inside it, pass a redact hook. It runs inside your process, on the way out, before anything is queued for delivery.

  1. It receives the field’s text and which field it is, and returns the text to send. The kind is "request" or "response".

    app/redaction.py
    import re
    EMAIL = re.compile(r"[\w.+-]+@[\w-]+\.[\w.]+")
    CARD = re.compile(r"\b\d{13,19}\b")
    def redact(text: str, kind: str) -> str:
    text = EMAIL.sub("[email]", text)
    text = CARD.sub("[card]", text)
    return text
  2. app/main.py
    metergraph.init(redact=redact)

    There is no environment variable for this. A redaction rule is code, and it has to be reviewed like code.

  3. The request field is the serialized request, not just the user’s message, so a rule written against plain prose can miss a value that appears inside JSON. Feed your function a real captured request body in a unit test and assert on what survives.

What it costs you. Nothing structural: redacted rows are still replayable and still eligible for datasets. What you lose is fidelity. Analysis replays what was stored, so if your rule removes something the model actually needed, the replay is not a replay of the original call. Redact values, not structure.

Two more things happen that you do not control from the SDK, and it is worth knowing them before you decide how hard to redact:

  • Credential scrubbing at ingest. Keys named like credentials (authorization, api_key, token, cookie, password, client_secret and similar) are removed from captured request and response JSON before it is written. This is a safety net for accidents, not a privacy control. It matches on key names, so a secret sitting in a free-text field survives it.
  • Truncation. The SDK truncates each field to text_max_bytes, 1 MiB by default, and the server truncates each stored field to 100 KiB. So the size of what is stored is smaller than the size of what is sent. Raising text_max_bytes raises what leaves your process and does not raise what is kept.
You wantControlAnalysis and datasetsTrace text
No prompt text leaves the processcapture_text=False at init, or METERGRAPH_CAPTURE_TEXT=0Unavailable
One sensitive route stays darkcapture_text=False on route() or trace()Unavailable for that routeNone for that route
Values removed, structure keptredact hookAvailableRedacted text
A server that cannot store content at allRun the open-source serverUnavailable