Monitoring
Error tracking, OpenTelemetry logs, and source maps on the same PostHog project as Analytics. Enabled when NEXT_PUBLIC_POSTHOG_KEY is set; host vars default to US Cloud. Health probes for uptime monitors.
Last updated on
5 min readOn This Page
The Three Pipelines
| Pipeline | Surface | What it captures |
|---|---|---|
| Errors | captureError (browser), captureServerError (Node), onRequestError (auto) | JS exceptions, server route/render/action errors, manually reported errors |
| Logs | log.{trace,debug,info,warn,error,fatal} from @syntaxkit/analytics/logger | Structured server logs via OTLP to ${NEXT_PUBLIC_POSTHOG_HOST}/i/v1/logs |
| Source maps | withPostHogConfig in apps/web/next.config.ts | Production source maps uploaded at build, then deleted locally |
Product events, identify, and session replay live on Analytics. This page is the ops half of the same PostHog project.
Error Tracking
Three surfaces feed PostHog error tracking: two you call, one automatic.
Client. Use captureError from @syntaxkit/analytics/client in any error boundary. The kit's global boundary at apps/web/app/global-error.tsx does this:
"use client";
import { useEffect } from "react";
import { captureError } from "@syntaxkit/analytics/client";
export default function GlobalError({
error,
}: {
error: Error & { digest?: string };
}) {
useEffect(() => {
captureError(error);
}, [error]);
// ...render fallback UI
}PostHog links each exception to the active session replay, so a dashboard error comes with the stack and the recent user activity that led to it.
Server. Use captureServerError(error, distinctId?, context?) from @syntaxkit/analytics/server when you catch an error and still want PostHog to see it. It awaits a flush so serverless invocations do not drop the capture:
import { captureServerError } from "@syntaxkit/analytics/server";
try {
await doSomethingRisky();
} catch (error) {
await captureServerError(error as Error, userId, { route: "/api/risky" });
throw error;
}Automatic. The onRequestError hook in apps/web/instrumentation.ts runs on every unhandled server render, route handler, or server action error. You do not wrap those paths in try/catch for capture to work.
You do not need a Sentry-style wrapper for unhandled server exceptions. Next's onRequestError covers those boundaries. Use manual captureServerError only when you already handled the exception and still want PostHog to know.
What the onRequestError hook does
On each error it runs three steps in order:
- Reads the PostHog
distinctIdandsessionIdfrom the request cookie withgetPostHogContext(cookieString), so the exception ties to the right user and session replay. - Calls
log.errorwith request and route context (path, method, router kind, route path/type, error name, truncated stack). - Calls
posthog.captureException(err, distinctId)and force-flushes the OTel logger so logs reach PostHog before the runtime exits.
export const onRequestError = async (err, request, context) => {
if (analyticsEnabled && process.env.NEXT_RUNTIME === "nodejs") {
const ctx = getPostHogContext(cookieString);
log.error(`Server request error: ${err.message}`, {
posthogDistinctId: ctx.distinctId,
sessionId: ctx.sessionId,
attributes: {
error_name: err.name,
error_stack: err.stack?.slice(0, 500) ?? "",
request_path: request.path,
request_method: request.method,
router_kind: context.routerKind,
route_path: context.routePath,
route_type: context.routeType,
},
});
await posthog.captureException(err, ctx.distinctId);
await loggerProvider?.forceFlush();
}
};Structured Logging
Use log from @syntaxkit/analytics/logger. It wraps an OpenTelemetry logger and ships OTLP records to PostHog Logs. Six severity levels, three optional fields:
import { log } from "@syntaxkit/analytics/logger";
log.info("Stripe webhook processed", {
posthogDistinctId,
sessionId,
attributes: { eventId, eventType, outcome },
});| Level | Use for |
|---|---|
trace | Highest-frequency, lowest-importance traces (rarely used). |
debug | Development tracing. Quiet in production. |
info | Normal operations: webhooks processed, tasks completed. |
warn | Recoverable problems: rate-limit hit, retryable failure. |
error | Server errors that produced an end-user-visible failure. |
fatal | Unrecoverable failures that need immediate attention. |
Pass posthogDistinctId and sessionId when you have them. They link a log line to the user who triggered it. attributes is freeform structured data; PostHog Logs shows those keys as filterable columns.
The LoggerProvider lives in apps/web/instrumentation.ts with a BatchLogRecordProcessor, so logs ship in batches. The Stripe webhook handler and the auto error hook are the reference call sites.
How logs are batched and flushed
apps/web/instrumentation.ts builds the provider only when analytics env is set. Route handlers that must not lose a log (webhooks, onRequestError) call loggerProvider?.forceFlush() after the critical write.
Source Maps
When POSTHOG_API_KEY and POSTHOG_PROJECT_ID are set, @posthog/nextjs-config wraps the Next config in apps/web/next.config.ts. At next build it uploads source maps, then deletes them locally so they never ship in the runtime image:
export default process.env.POSTHOG_API_KEY && process.env.POSTHOG_PROJECT_ID
? withPostHogConfig(composedConfig, {
personalApiKey: process.env.POSTHOG_API_KEY,
projectId: process.env.POSTHOG_PROJECT_ID,
host: process.env.NEXT_PUBLIC_POSTHOG_HOST,
sourcemaps: { enabled: true, deleteAfterUpload: true },
})
: composedConfig;These two env vars must be present at next build time, not only at runtime. Pass them as build-time vars on Vercel (Environment Variables), Fly ([build.args] in fly.toml), Render (envVars in render.yaml), or Docker (--build-arg). If they are runtime-only, the upload silently no-ops and PostHog stack traces stay minified.
The Reverse Proxy
Client init uses api_host: "/ingest" instead of the public PostHog host. When POSTHOG_PROXY_INGEST_HOST and POSTHOG_PROXY_ASSET_HOST are set, apps/web/next.config.ts rewrites those paths to your PostHog endpoints:
async rewrites() {
if (
process.env.POSTHOG_PROXY_INGEST_HOST &&
process.env.POSTHOG_PROXY_ASSET_HOST
) {
return [
{
source: "/ingest/static/:path*",
destination: `${process.env.POSTHOG_PROXY_ASSET_HOST}/static/:path*`,
},
{
source: "/ingest/:path*",
destination: `${process.env.POSTHOG_PROXY_INGEST_HOST}/:path*`,
},
];
}
return [];
},Same-origin requests avoid many ad-blockers, and the kit's strict CSP does not need posthog.com in connect-src. The proxy is optional. Without the two env vars, rewrites no-op and the client uses NEXT_PUBLIC_POSTHOG_HOST directly (CSP must allow that origin).
Health Probes
Uptime monitors should hit the plain Next routes. Matching oRPC procedures (readinessCheck, livenessCheck) exist on /rpc for in-app checks; monitors that do not speak RPC use the HTTP paths below.
| Endpoint | Purpose | Used by |
|---|---|---|
/api/healthz | Liveness. Always returns 200 if the process is up. | Manual / custom probes |
/api/health | Readiness. Returns 503 when the app is misconfigured or the database ping fails. | Docker Compose healthcheck, Fly [[http_service.checks]], Render healthCheckPath |
See API for procedure-level detail.
Env Vars At A Glance
| Variable | When required | What it controls |
|---|---|---|
NEXT_PUBLIC_POSTHOG_KEY | Set to enable; omit to disable | Turns analytics and monitoring on. Host vars alone do nothing. |
NEXT_PUBLIC_POSTHOG_HOST, NEXT_PUBLIC_POSTHOG_UI_HOST | Optional | Ingest and UI hosts. Default to US Cloud when unset. |
POSTHOG_PROXY_INGEST_HOST, POSTHOG_PROXY_ASSET_HOST | Optional | Same-origin reverse proxy via /ingest/* |
POSTHOG_API_KEY, POSTHOG_PROJECT_ID | Optional, build-time | Source-map upload at next build |
Where To Go Next
Security
Abuse-protection paths that write through the same log helper.
API
Health endpoints in context, plus the Stripe webhook as the structured-logging reference.
Also useful: Analytics for product events on the same PostHog project, and Deployment for where build-time POSTHOG_API_KEY and POSTHOG_PROJECT_ID are set per host.
Analytics
PostHog product analytics, web analytics, and session replay. Typed event catalog, identify-on-login, org groups, and a same-origin /ingest proxy. Enabled when NEXT_PUBLIC_POSTHOG_KEY is set; host vars default to US Cloud. Error tracking and logs live on Monitoring.
Security
Five layers on every request: edge headers, Better Auth, oRPC middleware, Upstash abuse policy, and Zod. Most run by default. Upstash and Turnstile need env vars. Defaults, limits, and a pre-launch checklist.
