Observability Setup
Purpose
Detect production errors and performance regressions proactively. Every silent failure costs a user; observability turns silence into signal.
Universal — error capture, CWV reporting, structured logging, console.log replacement, and alert thresholds apply to any frontend. Only the error-tracking SDK and framework-specific integration hooks differ.
Procedure
Install and initialize an error-tracking SDK (validation loop)
- Install the SDK and run the framework's bootstrap wizard — it initializes capture across every runtime (client, server, and edge each need their own init)
- Trigger a test error in dev mode (e.g.,
throw new Error('observability test')in a request handler) - Verify the error appears in the dashboard within 30 seconds; if not, check init config and re-trigger
- Confirm the stack trace is un-minified — if it shows
a.b.c, source-map upload isn't wired (the wizard sets the auth token; CI must upload maps on build). Upload maps privately to the SDK but don't serve them publicly (seesecurity-audit) - Tag each build with a release + git commit so errors attribute to a version and "suspect commits" work — set the release in both SDK init and the deploy step (see
cicd-pipeline) - Do not proceed to step 2 until end-to-end capture is confirmed working
- See Implementation for the exact wizard command and generated files
Wire error boundaries to the SDK
- Every error-boundary component must report to the SDK (
captureException) in addition to rendering fallback UI - Route-level boundaries catch first; the global / last-resort boundary fires only when the root itself throws
- Coordinate boundary placement with
async-ux-states
- Every error-boundary component must report to the SDK (
Capture server-side and mutation errors
- Server request handlers: use the SDK's automatic request-error hook where the framework provides one
- Server mutations / actions are often NOT auto-instrumented — wrap them explicitly (see Implementation)
- Any
catchthat returns gracefully MUST report the error before returning — otherwise the failure is silent in production
Add user context (anonymized) AND scrub auto-captured PII
- Attach anonymized user context (id only — never email/name) so errors group by impact (1 user vs 1000)
- The SDK captures more than you set: request URLs/headers, breadcrumbs, and (with Session Replay) DOM and input values — these leak tokens, auth headers, and form data. Configure
beforeSendscrubbing + the platform's data-scrubbing, and mask text/inputs in Session Replay. "I didn't callsetUserwith an email" is not enough. (see Implementation)
Capture Core Web Vitals
- Use the framework's CWV reporting hook (or the
web-vitalslibrary directly) at a top-level layout - Forward LCP / INP / CLS / TTFB / FCP to your analytics (Sentry, Vercel Analytics, custom)
- Track over time, alert on regression
- Use the framework's CWV reporting hook (or the
Structured logging utility
- Replace
console.logwith aloggerutility - JSON format with
level(debug / info / warn / error),timestamp,route,userId(anonymized) - Single source of truth — easy to swap backends (Sentry → Datadog → custom)
- Replace
Strip
console.logfrom production codegrep -rn 'console\.log' src/→ 0 in production code (debugger / test files OK)- Better: ESLint rule
no-console: ['error', { allow: ['warn', 'error'] }]
7b. Control sampling, quota, and noise
- Errors are cheap; traces and Session Replay are not — set
tracesSampleRateandreplaysSessionSampleRatedeliberately (sample a fraction in prod) or quota and cost blow up - Filter known noise (
ignoreErrors/denyUrls): browser-extension errors,ResizeObserver loopwarnings, network aborts — left in, they bury real signal and burn quota - Rate-limit or group spammy errors so one broken component doesn't flood the dashboard
- Set alert thresholds
- Alert on error-rate spikes and CWV regressions past target — configured in the observability platform, not the app. Alert on patterns, not every error.
Completion Criteria
- Sentry captures errors from dev test trigger
- Production stack traces are un-minified (source maps uploaded, not served publicly)
- Releases tagged with git commit (errors attribute to a version)
- All
error.tsxfiles wired toSentry.captureException - Server Actions wrapped in
withServerActionInstrumentation - Auto-captured PII scrubbed (
beforeSend+ data scrubbing; Session Replay masked) -
useReportWebVitalsreporting CWV - Trace/Replay sampling rates set; known noise filtered (
ignoreErrors/denyUrls) -
console.logcount insrc/(production code) = 0 - Structured logger used for all in-app logging
- Alert thresholds configured
Output
- SDK config files: framework-specific (e.g.,
sentry.server.config.ts,instrumentation.ts,instrumentation-client.ts) - Logger utility:
src/lib/logger.tswith JSON structured output + level support - CWV integration:
useReportWebVitals(or equivalent) in root layout, forwarding to analytics/Sentry - Alert config: documented in
docs/observability-alerts.md— thresholds, channels, runbook links - Smoke-test commit (after install):
chore(observability): wire <SDK> + verify test capture
Implementation
React + Next.js (default — Sentry)
- Wizard:
npx @sentry/wizard@latest -i nextjs— createssentry.server.config.ts,sentry.edge.config.ts,instrumentation-client.ts(replaces the oldersentry.client.config.ts),instrumentation.ts; wrapsnext.config.tsinwithSentryConfig - Error boundaries: in
error.tsxandglobal-error.tsx,useEffect(() => Sentry.captureException(error), [error]); route-levelerror.tsxcatches first,global-error.tsxonly when the root layout itself throws - Server error capture:
onRequestError = Sentry.captureRequestErrorininstrumentation.ts(route handlers / middleware, automatic) - Server Actions: NOT auto-captured — wrap each with
Sentry.withServerActionInstrumentation(name, options?, callback) - User context:
Sentry.setUser({ id: anonymizedUserId })— id only, never PII (email/name) - Source maps: the wizard sets
SENTRY_AUTH_TOKEN;withSentryConfiguploads on build; keepproductionBrowserSourceMaps: falseso maps aren't public (seesecurity-audit) - PII scrubbing:
beforeSendto drop sensitive fields; enable server-side data scrubbing;replayIntegration({ maskAllText: true, blockAllMedia: true }) - Sampling/noise:
tracesSampleRate,replaysSessionSampleRate/replaysOnErrorSampleRate;ignoreErrors/denyUrlsfor noise;tracePropagationTargetsto link frontend spans to your API - Release:
releasein init +SENTRY_RELEASE(git SHA) on deploy for suspect-commit attribution - CWV:
useReportWebVitalshook → forward to analytics
Other stacks
- Vue / Nuxt:
@sentry/vue+@sentry/nuxt(Nuxt module auto-installs everything) - SvelteKit:
@sentry/sveltekit— wrapshandlehook for server-side capture - Angular:
@sentry/angular— ErrorHandler injectable wires to all uncaught errors - Alternatives to Sentry: Datadog RUM, New Relic Browser, Honeybadger, Bugsnag, LogRocket, Highlight — all support framework-agnostic browser SDKs
- Universal:
web-vitalslibrary reports CWV from any client; structured JSON logging is framework-agnostic; alert thresholds (error rate, p95 latency, CWV regression) configured in the observability platform, not the app
Related skills
async-ux-states— error.tsx components must wire Sentry.captureExceptioncicd-pipeline— Sentry release tagging on deployrendering-performance— CWV reported here for the field-data picture
Reference
- Key insight encoded:
global-error.tsxis last-resort only — route-levelerror.tsxcatches first. Anycatchthat returns gracefully (Server Actions, route handlers) MUST callcaptureExceptionbefore returning, otherwise the failure is silent in production. Wire CWV reporting from day one — performance regressions are invisible without it. Three setup gaps that quietly defeat observability: minified traces (upload source maps — privately, not public), the SDK auto-capturing PII (scrub viabeforeSend+ mask Session Replay), and unbounded trace/replay sampling (cost blowup). Tag releases with the git commit so errors attribute to a version.