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.
Turn content capture off everywhere
Section titled “Turn content capture off everywhere”The blunt instrument, and the right one if no prompt text may leave your process at all.
import metergraph
metergraph.init(capture_text=False)import * as mg from "metergraph";
mg.init({ captureText: false });Or without touching code, which is usually the better answer because it can differ per environment:
METERGRAPH_CAPTURE_TEXT=0The 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.
Narrow it to one route or one operation
Section titled “Narrow it to one route or one operation”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.
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): ...await mg.route("patient-intake", async () => { return client.chat.completions.create(...);}, { captureText: false });mg.trace() accepts the same option for a whole multi-call operation.
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.
Redact in your own process
Section titled “Redact in your own process”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.
-
Write the function
Section titled “Write the function”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 reEMAIL = 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 textsrc/redaction.ts const EMAIL = /[\w.+-]+@[\w-]+\.[\w.]+/g;const CARD = /\b\d{13,19}\b/g;export function redact(text: string, kind: "request" | "response"): string {return text.replace(EMAIL, "[email]").replace(CARD, "[card]");} -
Register it at init
Section titled “Register it at init”app/main.py metergraph.init(redact=redact)src/instrument.ts mg.init({ redact });There is no environment variable for this. A redaction rule is code, and it has to be reviewed like code.
-
Test it against a real prompt
Section titled “Test it against a real prompt”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.
What happens after the row leaves you
Section titled “What happens after the row leaves you”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_secretand 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. Raisingtext_max_bytesraises what leaves your process and does not raise what is kept.
Choosing
Section titled “Choosing”| You want | Control | Analysis and datasets | Trace text |
|---|---|---|---|
| No prompt text leaves the process | capture_text=False at init, or METERGRAPH_CAPTURE_TEXT=0 | Unavailable | — |
| One sensitive route stays dark | capture_text=False on route() or trace() | Unavailable for that route | None for that route |
| Values removed, structure kept | redact hook | Available | Redacted text |
| A server that cannot store content at all | Run the open-source server | Unavailable | — |
See also
Section titled “See also”- Captured fields for the full row
- Configuration for every option and environment variable
- Build a dataset, which needs captured content