Error Logging & Sentry Integration Architecture
This document describes how errors flow from application code to Sentry, how the logger and Sentry interact, and flags known issues in the current implementation.
Overview
The codebase uses a provider registration pattern to connect structured loggers to Sentry without creating hard import dependencies. The key principle: packages define register*Provider() functions, and host apps wire up Sentry at initialization.
There are three error reporting mechanisms:
| Mechanism | Where | What it does |
|---|---|---|
logger.error() | Server + Client | Structured console log + auto-captures to Sentry via registered provider |
useSentryToast() | Client only | Shows destructive toast + captures to Sentry directly |
Sentry.captureException() | Server + Client | Direct Sentry capture (bypasses logger) |
Core Logger Implementations
Server Logger (packages/core/src/server/logger.ts)
Logger.error(message, context?, error?)
├── console.error(JSON) in production (Cloud Logging / Vercel)
├── console.error(readable) in development
└── _errorCaptureProvider(error ?? message, { tags, contexts }) ← Sentry
Key behavior:
error()always captures to Sentry when provider is registered, even without an Error object- Without an Error object, the message string is passed to
Sentry.captureException(creates a synthetic exception) warn()captures via_messageCaptureProvider(Sentry.captureMessagewith level "warning")info()anddebug()never capture to Sentry- The
errorparameter isunknownand normalized viatoError()(packages/core/src/shared/errors.ts)
Client Logger (packages/core/src/web/logger.ts)
ClientLogger.error(message, context?, error?)
├── addBreadcrumb({ type: "error", ... }) ← Sentry breadcrumb trail
├── _errorCaptureProvider(error ?? message, { tags, contexts }) ← Sentry
└── console.error(readable) in development ONLY
Key differences from server logger:
- Adds Sentry breadcrumbs on every log call (debug/info/warn/error)
- Only outputs to console in non-production (dev) environments
errorparameter is typed asError(notunknown)
Log Level to Sentry Mapping
| Level | Console Output | Sentry Capture | Sentry Breadcrumb |
|---|---|---|---|
debug | Dev only (both) | Never | Client only |
info | Always (server), Dev only (client) | Never | Client only |
warn | Always (server), Dev only (client) | captureMessage (warning level) | Client only |
error | Always (server), Dev only (client) | captureException | Client only |
Provider Registration Flow
Server-Side (instrumentation.ts)
All Next.js apps follow the same pattern in instrumentation.ts:
export async function register() {
const env = getAppEnvironment();
if (env.ENVIRONMENT === "cloud" && env.SENTRY_DSN) {
const Sentry = await import("@sentry/nextjs");
Sentry.init({ dsn, sampleRate: 1.0, tracesSampleRate: 0.2, ... });
// Wire logger -> Sentry
registerErrorCaptureProvider(Sentry.captureException);
registerMessageCaptureProvider(createSentryMessageCaptureProvider(Sentry));
}
}
Apps with this pattern: library, hello, admin, branded-video-flow, links
Express services (video-processor, sync-worker) follow the same pattern in their src/index.ts.
Client-Side (Two-Phase Init)
Phase 1: sentry.client.config.ts - Runs at script load time (before React):
if (typeof window !== "undefined") {
const env = getAppPublicEnvironment();
if (env.ENVIRONMENT === "cloud" && env.NEXT_PUBLIC_SENTRY_DSN) {
Sentry.init(buildClientSentryOptions({ dsn, ... }));
}
}
Phase 2: Client providers - Registers logger providers. Registration location varies by app:
// library, hello, admin, links: inside useEffect (runs after first render)
useEffect(() => {
registerBreadcrumbProvider((b) => Sentry.addBreadcrumb(b));
registerErrorCaptureProvider(Sentry.captureException);
registerMessageCaptureProvider(createSentryMessageCaptureProvider(Sentry));
}, []);
// branded-video-flow: at module level (before component renders — preferred)
registerSentryProvider({ addBreadcrumb: Sentry.addBreadcrumb, captureException: Sentry.captureException });
registerBreadcrumbProvider((b) => Sentry.addBreadcrumb(b));
registerErrorCaptureProvider(Sentry.captureException);
registerMessageCaptureProvider(createSentryMessageCaptureProvider(Sentry));
registerLayoutTelemetrySentry(Sentry);
The module-level pattern (used in BVF) is preferred because registration is guaranteed to happen before any component renders. The useEffect pattern delays registration until after the first render — components that call getLogger().error() synchronously on mount could theoretically fire before providers are registered in the useEffect approach.
Apps with client registration: library, hello, admin, links, branded-video-flow
useSentryToast
useSentryToast() (packages/core/src/web/use-sentry-toast.tsx) combines user-visible error toasts with Sentry capture:
const { toast } = useSentryToast();
toast.error("Operation failed", {
description: "Could not save",
error: caughtError,
context: { flowId },
tags: { action: "save" },
});
Internal flow:
toast.error(message, options)
├── baseToast({ title, description, variant: "destructive" }) ← User sees toast
└── Sentry.captureException(error ?? new Error(message), ...) ← Direct Sentry call
~20 files use useSentryToast correctly across app-hello, app-library, app-admin-library, and the hello/admin apps.
Client Sentry Configuration
buildClientSentryOptions() (packages/core/src/web/sentry.ts) configures:
| Feature | Setting | Notes |
|---|---|---|
| Error capture | sampleRate: 1.0 | 100% of errors captured |
| Performance | tracesSampleRate: 0.2 | 20% of transactions |
| Console capture | captureConsoleIntegration(["error", "warn"]) | Captures console.error/warn to Sentry |
| Session replay | replaysOnErrorSampleRate: 0.1-0.5 | Per-app configuration |
| Browser profiling | profilesSampleRate: 0.1 | 10% of sessions |
| Extension filtering | beforeSend | Drops browser extension errors |
| Third-party filtering | beforeSend | Drops errors from only third-party frames |
| Network enrichment | beforeSend | Adds online/offline status as tag |
| Clerk filtering | ignoreErrors | Drops Clerk dev key warnings in non-prod |
Error Boundary Pattern
Error boundaries use direct Sentry.captureException to obtain the eventId needed for feedback dialogs:
// packages/core/src/web/global-error.tsx
useEffect(() => {
Sentry.captureException(error, {
tags: serviceName ? { service: serviceName } : undefined,
});
}, [error]);
Components that need the Sentry event ID for feedback dialogs (e.g., DraftErrorState, RecordingSetupScreen) call Sentry.captureException directly because logger.error() does not return the event ID.
Data Flow Diagrams
Server-Side Error Flow
Application code
│
├─── logger.error("msg", ctx, err)
│ ├── console.error(JSON) → Cloud Logging / Vercel Logs
│ └── Sentry.captureException(err) → Sentry (via registered provider)
│
├─── logger.warn("msg", ctx)
│ ├── console.warn(JSON) → Cloud Logging / Vercel Logs
│ └── Sentry.captureMessage("msg", warn) → Sentry (via registered provider)
│
└─── Sentry.captureException(err) → Sentry (direct, bypasses logger)
(used in: test endpoints, Express error handlers)
Client-Side Error Flow
Application code
│
├─── logger.error("msg", ctx, err)
│ ├── Sentry.addBreadcrumb(...) → Breadcrumb trail
│ ├── Sentry.captureException(err) → Sentry (via registered provider)
│ └── console.error(readable) → Browser console (dev only)
│
├─── toast.error("msg", { error }) (via useSentryToast)
│ ├── Toast notification → User sees destructive toast
│ └── Sentry.captureException(err) → Sentry (direct call)
│
├─── Sentry.captureException(err) → Sentry (direct)
│ (used in: error boundaries, feedback dialog components)
│
└─── console.error("...") → Sentry (via captureConsoleIntegration)
(only in production where Sentry is initialized)
Key Source Files
| File | Role |
|---|---|
packages/core/src/server/logger.ts | Server Logger class with provider registration |
packages/core/src/web/logger.ts | Client ClientLogger class with breadcrumbs + provider registration |
packages/core/src/server/sentry.ts | buildServerSentryOptions, createSentryMessageCaptureProvider |
packages/core/src/web/sentry.ts | buildClientSentryOptions, client event filtering, createSentryMessageCaptureProvider |
packages/core/src/web/use-sentry-toast.tsx | useSentryToast() hook - toast + Sentry in one call |
packages/core/src/web/global-error.tsx | Shared Next.js global error boundary with Sentry capture |
packages/core/src/shared/errors.ts | toError() - normalizes unknown values to Error instances |
apps/*/instrumentation.ts | Server-side Sentry init + provider registration |
apps/*/sentry.client.config.ts | Client-side Sentry init (phase 1) |
apps/*/_providers/client-providers.tsx | Client-side provider registration (phase 2) |
Architecture Strengths
- Decoupled via providers: Logger works without Sentry; Sentry integration is opt-in via registration. Packages never import
@sentry/*directly. - Consistent initialization: All apps follow the same
instrumentation.ts+sentry.client.config.ts+client-providers.tsxpattern. - Auto-capture on error/warn: Once providers are registered,
logger.error()andlogger.warn()automatically capture without additional code. - Smart client filtering: Browser extension errors, third-party-only errors, and Clerk dev warnings are filtered before reaching Sentry.
- Breadcrumb trail: Client logger adds breadcrumbs on every log call, providing context leading up to errors.
- Environment-aware: Sentry only initializes in cloud environments with valid DSNs. Local dev gets console-only logging.
Decision Guide: Which Error Reporting Mechanism to Use
Need to report an error?
├── Server-side code?
│ └── logger.error(message, context, error)
│ Auto-captures to Sentry, logs to Cloud Logging.
│
├── Client component with user-visible error?
│ ├── Need a toast notification?
│ │ └── useSentryToast().toast.error(message, { error, context })
│ │ Shows toast + captures to Sentry.
│ │
│ └── No toast needed (e.g., error boundary)?
│ └── Need Sentry eventId for feedback dialog?
│ ├── YES → Sentry.captureException(error) directly
│ │ (Don't also call logger.error — causes double capture)
│ └── NO → logger.error(message, context, error)
│ Auto-captures to Sentry via provider.
│
└── Package code (no direct Sentry import)?
└── logger.error(message, context, error)
Captures to Sentry only if host app registered providers.
Related Documentation
- Client-Side Provider Registration - The provider pattern used for Sentry integration
- Server-Side Providers and DI - Server-side dependency injection patterns