Masking
@grounded/tracing lets you transform a trace before it is sent.
The hook is:
mask?: (trace: TraceWrite) => TraceWriteThis happens at flush time, right before the trace is posted to /api/traces/batch.
Example
import { GroundedTracing } from '@grounded/tracing';
const tracing = new GroundedTracing({
publishableKey: process.env.GROUNDED_PUBLISHABLE_KEY!,
mask(trace) {
const clone = structuredClone(trace);
if (clone.input && typeof clone.input === 'object') {
clone.input = redactObject(clone.input);
}
if (clone.output && typeof clone.output === 'object') {
clone.output = redactObject(clone.output);
}
clone.spans = (clone.spans ?? []).map((span) => ({
...span,
input: span.input && typeof span.input === 'object'
? redactObject(span.input)
: span.input,
output: span.output && typeof span.output === 'object'
? redactObject(span.output)
: span.output,
}));
return clone;
},
});
function redactObject(value: unknown) {
const json = JSON.stringify(value)
.replace(/[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}/gi, '[EMAIL]')
.replace(/\+?\d[\d\s\-()]{7,}\d/g, '[PHONE]');
return JSON.parse(json);
}What To Mask
Typical candidates:
- user prompts
- assistant outputs
- tool inputs and outputs
- span event attributes
- free-form metadata fields
What To Leave Alone
Avoid masking fields that you rely on for operational filtering:
resource.agentIdresource.snapshotIdresource.environmentresource.release- route names
- model names
- tags that are already non-sensitive
Recommended Approach
- use deterministic placeholders such as
[EMAIL]or[PHONE] - keep masking stable across spans and traces
- mask before data leaves your process
- test the masked output through
/api/traces/:traceId
Important Detail
The readback routes return the masked payload, not the pre-mask version. If something is missing after ingestion, first check your mask function.