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.

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:

ProviderError TrackingPerformanceSelf-HostableNotes
SentryYesYesYesBuilt-in, recommended for most apps
PostHogYesNoYesPlugin, doubles as analytics
SigNozYesYesYesPlugin, OpenTelemetry-native

Sentry is included out of the box. PostHog and SigNoz require installing plugins via the Makerkit CLI.

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 variables

The 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 in
monitoring.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:

  1. Add the provider name to the MONITORING_PROVIDERS enum in get-monitoring-provider.ts.
  2. Register a server MonitoringService in services/get-server-monitoring-service.ts.
  3. Register a React MonitoringProvider component in components/provider.tsx.
  4. Register a server instrumentation entry in instrumentation.ts — both register (to init the SDK) and onRequestError (to forward Tanstack Start request errors) are required.
  5. Register a client instrumentation entry in instrumentation-client.tsinit and captureException.

Best Practices

Do capture context with errors

// Good: Includes debugging context
monitoring.captureException(error, {
userId: user.id,
accountId: account.id,
action: 'updateBillingPlan',
planId: newPlanId,
});
// Less useful: No context
monitoring.captureException(error);

Don't capture expected validation errors

// Avoid: This clutters your error dashboard
if (!isValidEmail(email)) {
monitoring.captureException(new Error('Invalid email'));
return { error: 'Invalid email' };
}
// Better: Only capture unexpected failures
try {
await sendEmail(email);
} catch (error) {
monitoring.captureException(error, {
extra: { email: maskEmail(email) },
});
}

Next Steps

Choose a monitoring provider and follow its setup guide: