Content and sensitive data

August 21, 2026 · View on GitHub

The SDK never inspects function arguments, HTTP bodies, or model clients and captures no provider content automatically. It does export fields explicitly supplied by the caller. Input/output are the obvious content fields, but metadata, model parameters, status messages, and errors can also contain sensitive data.

Set LANGFUSE_CONTENT_CAPTURE_ENABLED=false, or configure DisableContentCapture, to drop SDK-supplied Input and Output while still recording every other field. Client.WithContentCapture can override that default for observations started on one client-scoped local context tree. Each observation retains the decision made at start for all later Update calls. The privacy boundary is deliberately narrow:

Data sourceDropped when content capture is disabledPassed to Mask
ObservationAttributes.Input and OutputYesYes, unless content capture is disabled
ObservationAttributes.MetadataNoYes, once as the complete map[string]any
TraceAttributes.MetadataNoYes, once as the complete map[string]any
Observation name/type, trace name, user/session IDs, tags, version, level, StatusMessage, model/parameters, usage, costs, prompt, and completion timeNoNo
RecordError(err) text and exception eventNoNo
Score metadataNoYes, once as the complete map[string]any
Score comment and valueNoNo
OpenTelemetry resource attributes (resource.Default/OTEL_RESOURCE_ATTRIBUTES in isolated mode; caller resource in borrowed mode)NoNo
Third-party OTel span attributes and eventsNoNo

Cross-process propagation sends attributes to every destination. WithBaggagePropagation places user ID, session ID, trace name, version, request-scoped environment, string metadata values, and the 32-hex trace ID of the current application root into W3C baggage on its context branch. Baggage is delivered by whatever propagator the application has installed, so these values travel with every outbound request that carries the context — third-party APIs and services that have nothing to do with Langfuse included — until the branch ends. Inbound metadata accepted by WithTraceAttributesFromBaggage passes through the configured Mask exactly once, like local trace metadata; the other propagated fields are identifiers and are never masked. Enable propagation only on paths where that disclosure is intended. Baggage diagnostics name fixed protocol members only; metadata key suffixes and unknown member names are user- or wire-controlled and appear in diagnostics as counts, never as text.

Disabling content capture does not make metadata, model parameters, status messages, or errors safe. RecordError exports err.Error() as the OTel status description, Langfuse status message, and exception-event message. Use payload-free error values or sanitize an error before passing it to RecordError; never put credentials, PHI, prompts, or completions in an error or StatusMessage.

WithContentCapture is local process state, not an authorization system or a cross-process propagation mechanism. Call it only after the application has resolved its own access policy. It does not change score delivery: score metadata still passes through Mask, while score comments and values remain outside both the content-capture and masking controls.

Mask receives a MaskField with each SDK value shown in the table. A metadata masker must return a map[string]any; returning another type omits that metadata. The SDK calls the masker synchronously. It must be fast, non-blocking, and concurrency-safe. Copy and recursively redact maps and slices rather than mutating caller-owned data:

cfg := langfuse.ConfigFromEnv()
cfg.Mask = redactSDKValue

func redactSDKValue(field langfuse.MaskField, value any) any {
	switch field {
	case langfuse.MaskObservationInput, langfuse.MaskObservationOutput:
		return "[redacted]"
	case langfuse.MaskTraceMetadata, langfuse.MaskObservationMetadata, langfuse.MaskScoreMetadata:
		return redactMetadata(value)
	default:
		return nil
	}
}

func redactMetadata(value any) any {
	switch value := value.(type) {
	case map[string]any:
		redacted := make(map[string]any, len(value))
		for key, item := range value {
			switch strings.ToLower(key) {
			case "email", "customer_id", "authorization":
				redacted[key] = "[redacted]"
			default:
				redacted[key] = redactMetadata(item)
			}
		}
		return redacted
	case []any:
		redacted := make([]any, len(value))
		for index, item := range value {
			redacted[index] = redactMetadata(item)
		}
		return redacted
	default:
		return value
	}
}

The example fully replaces observation input and output. It assumes JSON-like map[string]any and []any metadata. A production masker must cover every concrete value type the application supplies, must be concurrency-safe, and should have tests proving its redaction policy.

These controls apply only to data supplied through this client; they never rewrite third-party OTel instrumentation, so configure or sanitize those instrumentors independently in borrowed mode (the client warns when content capture is disabled there). Resource attributes are also untouched: isolated mode preserves resource.Default (including OTEL_SERVICE_NAME and OTEL_RESOURCE_ATTRIBUTES) and borrowed mode preserves the caller's resource, so audit them before export.