Monitoring and Error Tracking in Makerkit
Set up error tracking and performance monitoring in your Tanstack Start Supabase SaaS app with Sentry, PostHog, or SigNoz.
Steps to configure monitoring
Learn how to configure monitoring in the Tanstack Start Supabase Starter Kit.
Understanding the Monitoring Architecture
Makerkit's monitoring system uses a provider-based architecture that lets you swap monitoring services without changing your application code. The system lives in the @kit/monitoring package and handles:
- Error tracking: Capture client-side and server-side exceptions
- Performance monitoring: Track server response times via OpenTelemetry instrumentation
- User identification: Associate errors with specific users for debugging
The architecture follows a registry pattern. When you set VITE_MONITORING_PROVIDER, Makerkit loads the appropriate service implementation at runtime:
MonitoringProvider (React context) │ ▼ Registry lookup │ ▼┌───────┴───────┐│ sentry ││ posthog ││ signoz │└───────────────┘This means your components interact with a consistent MonitoringService interface regardless of which provider you choose.
Supported Monitoring Providers
Makerkit provides first-class support for these monitoring providers:
| Provider | Error Tracking | Performance | Self-Hostable | Notes |
|---|---|---|---|---|
| Sentry | Yes | Yes | Yes | Built-in, recommended for most apps |
| PostHog | Yes | No | Yes | Plugin, doubles as analytics |
| SigNoz | Yes | Yes | Yes | Plugin, OpenTelemetry-native |
Sentry is included out of the box. PostHog and SigNoz require installing plugins via the Makerkit CLI.
Custom providers
You can add support for any monitoring service by implementing the MonitoringService interface and registering it in the provider registry. See Adding a custom monitoring provider below.
Configuring Your Monitoring Provider
Set these environment variables to enable monitoring:
# Required: Choose your provider (sentry, posthog, or signoz)VITE_MONITORING_PROVIDER=sentry# Provider-specific configuration# See the individual provider docs for required variablesThe VITE_MONITORING_PROVIDER variable determines which service handles your errors. Leave it empty to disable monitoring entirely (errors still log to console in development).
What Gets Monitored Automatically
Once configured, Makerkit captures errors without additional code:
Client-side exceptions
The MonitoringProvider component wraps your app and captures uncaught exceptions in React components. This includes:
- Runtime errors in components
- Unhandled promise rejections
- Errors thrown during rendering
Server-side exceptions
The Tanstack Start server entry forwards unhandled request errors to the configured provider. Makerkit wires this in apps/web/src/server.ts: it registers the provider once at startup, then wraps the request handler so any thrown error is reported before being rethrown.
import { onRequestError, registerMonitoringInstrumentation,} from '@kit/monitoring/instrumentation';// Load the configured monitoring provider once at server startup.// No-op when no provider is configured.void registerMonitoringInstrumentation();// ...export function createServerEntry(entry: ServerEntry): ServerEntry { return { async fetch(...args) { try { return await entry.fetch(...args); } catch (error) { // Forward unhandled request errors, then rethrow so the framework // still produces its own error response. await onRequestError(error, args[0]); throw error; } }, };}This captures errors thrown from server functions and Tanstack Start server routes.
Client-side exceptions
The browser counterpart lives in apps/web/src/client.tsx, which runs before hydration:
import { registerClientMonitoringInstrumentation } from '@kit/monitoring/instrumentation-client';// Install global error/unhandledrejection handlers before hydration so client// errors reach the configured monitoring provider.registerClientMonitoringInstrumentation();registerClientMonitoringInstrumentation installs provider-agnostic window.onerror and unhandledrejection listeners, then lazily loads the configured provider in parallel with hydration. Errors that fire before the provider chunk is ready are buffered and replayed.
Avoiding duplicate reports
A server error that reaches the client can carry a digest property. Because onRequestError already captured it, your route errorComponent boundary should skip the client report when digest is present — pass null to useCaptureException:
useCaptureException(error.digest ? null : error);Manually Capturing Exceptions
For expected errors (like validation failures or API errors), capture them explicitly:
In server functions or server routes
import { createServerFn } from '@tanstack/react-start';import { authFunctionMiddleware } from '@kit/function-middleware/functions';import { getServerMonitoringService } from '@kit/monitoring/server';export const createProjectFunction = createServerFn({ method: 'POST' }) .middleware(authFunctionMiddleware) .handler(async ({ context: { user } }) => { try { // ... your logic } catch (error) { const monitoring = await getServerMonitoringService(); await monitoring.ready(); monitoring.captureException(error, { action: 'createProject', userId: user.id, }); throw error; // Re-throw or handle as needed } });In React Components
Use the useMonitoring hook for client-side error capture:
import { useMonitoring } from '@kit/monitoring/hooks';export function DataLoader() { const monitoring = useMonitoring(); async function loadData() { try { const response = await fetch('/api/data'); if (!response.ok) { throw new Error(`Failed to load data: ${response.status}`); } return response.json(); } catch (error) { monitoring.captureException(error, { component: 'DataLoader', }); throw error; } } // ...}The useCaptureException Hook
For error boundaries or components that receive errors as props:
import { useCaptureException } from '@kit/monitoring/hooks';export function ErrorDisplay({ error }: { error: Error }) { // Automatically captures the error when the component mounts useCaptureException(error); return ( <div> <h2>Something went wrong</h2> <p>{error.message}</p> </div> );}Identifying Users in Error Reports
Associate errors with users to debug issues faster. Makerkit's monitoring providers support user identification:
const monitoring = useMonitoring();// After user signs inmonitoring.identifyUser({ id: user.id, email: user.email, // Additional fields depend on your provider});Makerkit automatically identifies users when they sign in if you've configured the analytics/events system. The user.signedIn event triggers user identification in both analytics and monitoring.
Adding a Custom Monitoring Provider
Adding a provider involves five registration points — see the Custom Provider guide for the full walkthrough. The short version:
- Add the provider name to the
MONITORING_PROVIDERSenum inget-monitoring-provider.ts. - Register a server
MonitoringServiceinservices/get-server-monitoring-service.ts. - Register a React
MonitoringProvidercomponent incomponents/provider.tsx. - Register a server instrumentation entry in
instrumentation.ts— bothregister(to init the SDK) andonRequestError(to forward Tanstack Start request errors) are required. - Register a client instrumentation entry in
instrumentation-client.ts—initandcaptureException.
onRequestError is not optional
The client error boundary skips re-reporting any error that carries a digest because the server already captured it via onRequestError. A custom provider that omits this hook will silently drop every server error.
Telegram notifications
We wrote a tutorial showing how to add Telegram notifications for error monitoring: Send SaaS errors to Telegram.
Best Practices
Do capture context with errors
// Good: Includes debugging contextmonitoring.captureException(error, { userId: user.id, accountId: account.id, action: 'updateBillingPlan', planId: newPlanId,});// Less useful: No contextmonitoring.captureException(error);Don't capture expected validation errors
// Avoid: This clutters your error dashboardif (!isValidEmail(email)) { monitoring.captureException(new Error('Invalid email')); return { error: 'Invalid email' };}// Better: Only capture unexpected failurestry { await sendEmail(email);} catch (error) { monitoring.captureException(error, { extra: { email: maskEmail(email) }, });}Next Steps
Choose a monitoring provider and follow its setup guide:
- Configure PostHog (if you already use PostHog for analytics)
- Configure SigNoz (self-hosted, OpenTelemetry-native)