Monitoring and Error Tracking in Makerkit

Set up error tracking and performance monitoring in your Next.js 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 NEXT_PUBLIC_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)
NEXT_PUBLIC_MONITORING_PROVIDER=sentry
# Provider-specific configuration
# See the individual provider docs for required variables

The NEXT_PUBLIC_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

Next.js 15+ includes an instrumentation hook that captures server errors automatically. Makerkit hooks into this via instrumentation.ts:

import { type Instrumentation } from 'next';
export const onRequestError: Instrumentation.onRequestError = async (
err,
request,
context,
) => {
const { getServerMonitoringService } = await import('@kit/monitoring/server');
const service = await getServerMonitoringService();
await service.ready();
await service.captureException(
err as Error,
{},
{
path: request.path,
headers: request.headers,
method: request.method,
routePath: context.routePath,
},
);
};

This captures errors from Server Components, Server Actions, Route Handlers, and Middleware.

Manually Capturing Exceptions

For expected errors (like validation failures or API errors), capture them explicitly:

In Server Actions or Route Handlers

import { getServerMonitoringService } from '@kit/monitoring/server';
export async function createProject(data: FormData) {
try {
// ... your logic
} catch (error) {
const monitoring = await getServerMonitoringService();
await monitoring.ready();
monitoring.captureException(error, {
extra: {
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:

'use client';
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, {
extra: { component: 'DataLoader' },
});
throw error;
}
}
// ...
}

The useCaptureException Hook

For error boundaries or components that receive errors as props:

'use client';
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

To add a provider not included in Makerkit:

1. Implement the MonitoringService interface

import { MonitoringService } from '@kit/monitoring-core';
export class MyProviderMonitoringService implements MonitoringService {
private readyPromise: Promise<void>;
private readyResolver?: () => void;
constructor() {
this.readyPromise = new Promise((resolve) => {
this.readyResolver = resolve;
});
this.initialize();
}
async ready() {
return this.readyPromise;
}
captureException(error: Error, extra?: Record<string, unknown>) {
// Send to your monitoring service
myProviderSDK.captureException(error, { extra });
}
captureEvent(event: string, extra?: Record<string, unknown>) {
myProviderSDK.captureEvent(event, extra);
}
identifyUser(user: { id: string }) {
myProviderSDK.setUser(user);
}
private initialize() {
// Initialize your SDK
myProviderSDK.init({ dsn: process.env.MY_PROVIDER_DSN });
this.readyResolver?.();
}
}

2. Register the provider

Add your provider to the monitoring registries:

const MONITORING_PROVIDERS = [
'sentry',
'my-provider', // Add your provider
'',
] as const;
serverMonitoringRegistry.register('my-provider', async () => {
const { MyProviderMonitoringService } = await import('@kit/my-provider');
return new MyProviderMonitoringService();
});
monitoringProviderRegistry.register('my-provider', async () => {
const { MyProviderProvider } = await import('@kit/my-provider/provider');
return {
default: function MyProviderWrapper({ children }: React.PropsWithChildren) {
return <MyProviderProvider>{children}</MyProviderProvider>;
},
};
});

Best Practices

Do capture context with errors

// Good: Includes debugging context
monitoring.captureException(error, {
extra: {
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: