SOMA docs
Agent

Observability

Langfuse for LLM traces, Sentry for errors, PostHog for product, Axiom for logs.

SOMA is instrumented with four independent observability stacks. Each answers a different question. All are opt-in via env vars — missing a key is a no-op, never a crash.

StackPurposeWhere in code
LangfuseLLM trace tree per agent invocationpackages/agent/src/observability/langfuse.ts
SentryUncaught errors + source mapsapps/web/src/lib/sentry.ts, apps/web/src/instrumentation*.ts
PostHogProduct analytics (page views, feature usage)apps/web/src/components/analytics/posthog-provider.tsx
AxiomStructured pino logspackages/agent/src/logger.ts

Langfuse

Every agent path calls startTrace({ name, userId, sessionId, input, tags }), passes the trace handle through the stream, and calls trace.end({ output }) on finish. Tags encode the surface: ['web', 'chat'], ['bot', 'message'], ['workflow', 'gmail.ingest'], ['workflow', 'fact-extract'].

flushLangfuse() is called eagerly after every stream finishes. Vercel serverless functions freeze between invocations, so unflushed events are lost. Always flush before returning.

Sentry

Sentry's Next SDK has a long history of accidentally importing pages-router code paths into App Router bundles. We work around that with a lazy wrapper at apps/web/src/lib/sentry.ts:

export function captureException(err: unknown, ctx?: { tags?: Record<string, string> }): void {
  if (!process.env.SENTRY_DSN && !process.env.NEXT_PUBLIC_SENTRY_DSN) return;
  void import('@sentry/nextjs').then((Sentry) => Sentry.captureException(err, ctx)).catch(() => {});
}

All nine server-action / route-handler call sites use this wrapper. The error-boundary components (error.tsx, global-error.tsx) use dynamic import('@sentry/nextjs') inline.

PostHog

EU-hosted (eu.i.posthog.com). Initialized in a client provider inside apps/web/src/app/layout.tsx. Captures history_change page views + submit/click autocaptures. Inited only when NEXT_PUBLIC_POSTHOG_KEY is set.

Axiom / pino

Structured JSON logs via pino. Axiom is an optional transport — when AXIOM_TOKEN + AXIOM_DATASET are set, logs mirror to a dataset.

:::caution Pino transports spawn a worker thread via thread-stream. Inside Next.js runtime, Turbopack rewrites absolute module paths and the worker can't find its entry file. packages/agent/src/logger.ts detects process.env.NEXT_RUNTIME and falls back to sync stdout JSON. Never re-enable transports inside Next. :::

What to check when something breaks

  1. "Why did the agent reply that?" → Langfuse. Filter by tags: 'chat' and sort by timestamp.
  2. "Why did the request 500?" → Sentry. Issues grouped by stack trace.
  3. "What are users actually using?" → PostHog. Funnels on /app/*.
  4. "What did the ingestion workflow log?" → Axiom. Query soma-logs with app:agent filter.