Observability
Langfuse for the model, Sentry for the process, structured logs for everything else.
Three signals, each answering a different question.
Langfuse
Why did the assistant answer this way? Traces, retrieval, tool calls, evaluations.
Sentry
What broke? Exceptions and performance for the service itself.
Structured logs
What happened, in order? One JSON line per event, carrying a request id.
Request correlation
Every response carries a request id, generated at the outermost middleware so it exists even when the request fails before reaching a route. The same id appears in the log line, the Sentry event and the trace, which is what makes a report of "it failed around 3pm" actionable.
Each turn gets its own server-generated Langfuse trace id. The request id is kept
as trace metadata rather than reused, because a caller can supply it. Sentry
events carry the trace id as langfuse_trace_id, together with conversation_id
and channel, and so does each turn's turn.completed log line. Langfuse groups a conversation's turns
into one session.
The caller appears as one pseudonym everywhere: the Langfuse user, the Sentry
user and the user_id field of a log line. It is a keyed hash of their
user_ref, never the value itself — for a member, user_ref is the chat session
id, and that is also their bearer. The hash is keyed by ENCRYPTION_KEY; in dev
without one there is no pseudonym, so traces and Sentry events have no user and
log lines a null user_id.
One line per turn
Every chat turn ends with exactly one turn.completed log line, including turns that fail or
where the client hangs up:
{"event": "turn.completed", "outcome": "stop", "duration_ms": 4120,
"prompt_tokens": 2310, "completion_tokens": 188, "tool_calls": 1, "tool_errors": 0,
"retrieval_hits": 4, "request_id": "…", "conversation_id": "…", "langfuse_trace_id": "…"}outcome is the finish reason (stop, awaiting_confirmation, …), error (with
error_code), or cancelled. Build latency, error-rate and token dashboards from it.
Sampling
| Setting | Default | Notes |
|---|---|---|
LANGFUSE_SAMPLE_RATE | 1.0 | Fraction of turns traced. Lower it in prod if volume outgrows the plan. |
SENTRY_TRACES_SAMPLE_RATE | 0.1 | Performance only. Errors are always sent, and health probes are never sampled. |
Redaction
Non-negotiable
Access tokens, OTPs and PlanCover tool URLs never reach a prompt, graph state,
a log line, a trace or an error report. Redaction happens by whole key
segment, so a nested access_token is removed wherever it appears rather than
only at the top level.
Langfuse is an observability and evaluation tool. It is not the application conversation database — conversation state lives in Postgres.
What is worth alerting on
| Signal | Why it matters |
|---|---|
| Readiness failures | The service is up but cannot serve |
Tool error rate (turn.completed → tool_errors) | An upstream PlanCover API is degraded |
outcome=error rate by error_code | Turns failing in front of members |
p95 duration_ms | The member is watching a blank screen |
| Groundedness score | Answers are drifting from retrieved evidence |