Creating a Custom Monitoring Provider

Integrate LogRocket, Bugsnag, Datadog, or any monitoring service by implementing the MonitoringService interface.

The monitoring system uses a registry pattern that loads providers dynamically based on the NEXT_PUBLIC_MONITORING_PROVIDER environment variable. You can add support for LogRocket, Bugsnag, Datadog, or any other service.

Implement the MonitoringService Interface

The service is a thin wrapper around the SDK's capture / identify APIs — no SDK initialization here. Init happens in the instrumentation registrations further down.

packages/monitoring/logrocket/src/logrocket-monitoring.service.ts

import LogRocket from 'logrocket';
import { MonitoringService } from '@kit/monitoring-core';
export class LogRocketMonitoringService implements MonitoringService {
// The interface still requires `ready()`. Init runs in the instrumentation
// entrypoint, so this is a no-op for new providers.
ready() {
return Promise.resolve();
}
captureException(error: Error, extra?: Record<string, unknown>) {
LogRocket.captureException(error, { extra });
}
captureEvent(event: string, extra?: Record<string, unknown>) {
LogRocket.track(event, extra);
}
identifyUser(user: { id: string; email?: string; name?: string }) {
LogRocket.identify(user.id, {
email: user.email,
name: user.name,
});
}
}

Package Configuration

Create the package structure:

packages/monitoring/logrocket/package.json

{
"name": "@kit/logrocket",
"version": "0.0.1",
"private": true,
"exports": {
".": "./src/index.ts"
},
"dependencies": {
"@kit/monitoring-core": "workspace:*",
"logrocket": "^3.0.0"
}
}

packages/monitoring/logrocket/src/index.ts

export { LogRocketMonitoringService } from './logrocket-monitoring.service';

Register the Provider Type

Update the provider enum so NEXT_PUBLIC_MONITORING_PROVIDER=logrocket validates:

packages/monitoring/api/src/get-monitoring-provider.ts

const MONITORING_PROVIDERS = [
'sentry',
'logrocket', // Add your provider
'',
] as const;

Register the React Provider

The React provider injects your MonitoringService into the MonitoringContext so useMonitoring / useCaptureException can reach it from client components:

packages/monitoring/logrocket/src/provider.tsx

import { MonitoringContext } from '@kit/monitoring-core';
import { LogRocketMonitoringService } from './logrocket-monitoring.service';
const logrocket = new LogRocketMonitoringService();
export function LogRocketProvider({ children }: React.PropsWithChildren) {
return (
<MonitoringContext.Provider value={logrocket}>
{children}
</MonitoringContext.Provider>
);
}

packages/monitoring/api/src/components/provider.tsx

monitoringProviderRegistry.register('logrocket', async () => {
const { LogRocketProvider } = await import('@kit/logrocket/provider');
return {
default: function LogRocketProviderWrapper({ children }: React.PropsWithChildren) {
return <LogRocketProvider>{children}</LogRocketProvider>;
},
};
});

Register the Server Service

Used anywhere code calls getServerMonitoringService() (Server Actions, Route Handlers, jobs):

packages/monitoring/api/src/services/get-server-monitoring-service.ts

serverMonitoringRegistry.register('logrocket', async () => {
const { LogRocketMonitoringService } = await import('@kit/logrocket');
return new LogRocketMonitoringService();
});

Register the Server Instrumentation

This is the entry that initializes your SDK on the server and forwards Next.js request errors. onRequestError is required — without it, every server error is dropped (the client error.tsx already skips reports for errors with a digest).

packages/monitoring/api/src/instrumentation.ts

instrumentationRegistry.register('logrocket', async () => {
const { initializeLogRocketServerClient } = await import(
'@kit/logrocket/config/server'
);
return {
register: () => {
initializeLogRocketServerClient({
appId: process.env.LOGROCKET_APP_ID,
});
},
onRequestError: async (error, request, context) => {
const { getServerMonitoringService } = await import(
'../services/get-server-monitoring-service'
);
const service = await getServerMonitoringService();
await service.captureException(error as Error, {}, {
path: request.path,
headers: request.headers,
method: request.method,
routePath: context.routePath,
});
},
};
});

If your SDK provides a built-in Next.js request-error handler (the way @sentry/nextjs exposes captureRequestError), use it directly — it will produce richer context than a hand-rolled forward.

Register the Client Instrumentation

The client instrumentation entry initializes the browser SDK and provides a captureException that the provider-agnostic global handlers in instrumentation-client.ts will call. Do not register your own window.onerror / unhandledrejection listeners — the monitoring layer owns those, and adding more will double-report. If your SDK installs its own by default (e.g. Sentry's GlobalHandlers), disable that integration.

packages/monitoring/api/src/instrumentation-client.ts

clientInstrumentationRegistry.register('logrocket', async () => {
const [{ initializeLogRocketBrowserClient }, LogRocket] = await Promise.all([
import('@kit/logrocket/config/client'),
import('logrocket'),
]);
return {
init: () => {
initializeLogRocketBrowserClient({
appId: process.env.NEXT_PUBLIC_LOGROCKET_APP_ID,
});
},
captureException: (error, mechanism) => {
LogRocket.captureException(error as Error, {
tags: { mechanism },
});
},
};
});

The mechanism argument is one of onerror, onunhandledrejection, or react-error-boundary — useful for telling unhandled rejections from React boundary errors in your dashboard.

Environment Variables

Add your provider's configuration:

apps/web/.env.local

# Enable LogRocket as the monitoring provider
NEXT_PUBLIC_MONITORING_PROVIDER=logrocket
# LogRocket configuration
NEXT_PUBLIC_LOGROCKET_APP_ID=your-org/your-app

Example: Datadog Integration

Here's a complete example for Datadog RUM. The service is a thin wrapper; init lives in the client instrumentation entry below.

packages/monitoring/datadog/src/datadog-monitoring.service.ts

import { datadogRum } from '@datadog/browser-rum';
import { MonitoringService } from '@kit/monitoring-core';
export class DatadogMonitoringService implements MonitoringService {
ready() {
return Promise.resolve();
}
captureException(error: Error, extra?: Record<string, unknown>) {
datadogRum.addError(error, { ...extra });
}
captureEvent(event: string, extra?: Record<string, unknown>) {
datadogRum.addAction(event, extra);
}
identifyUser(user: { id: string; email?: string; name?: string }) {
datadogRum.setUser({
id: user.id,
email: user.email,
name: user.name,
});
}
}

packages/monitoring/datadog/src/datadog.client.config.ts

import { datadogRum } from '@datadog/browser-rum';
export function initializeDatadogBrowserClient() {
datadogRum.init({
applicationId: process.env.NEXT_PUBLIC_DATADOG_APP_ID!,
clientToken: process.env.NEXT_PUBLIC_DATADOG_CLIENT_TOKEN!,
site: process.env.NEXT_PUBLIC_DATADOG_SITE ?? 'datadoghq.com',
service: process.env.NEXT_PUBLIC_DATADOG_SERVICE ?? 'my-saas',
env: process.env.NEXT_PUBLIC_DATADOG_ENV ?? 'production',
sessionSampleRate: 100,
sessionReplaySampleRate: 20,
trackUserInteractions: true,
trackResources: true,
// Datadog can capture its own unhandled errors; turn it off because the
// monitoring layer already installs `window.onerror` / `unhandledrejection`.
forwardErrorsToLogs: false,
});
}

Then wire the client instrumentation and React provider as shown in the registration sections above.

Common Gotchas

  1. Don't double-install global handlers@kit/monitoring/instrumentation-client owns window.onerror and unhandledrejection. Disable any equivalent feature in your SDK's options (Sentry's GlobalHandlers, Datadog's forwardErrorsToLogs, etc.).
  2. onRequestError is required — the client error boundary skips reports for errors that carry a digest. Without a server-side onRequestError, those errors disappear.
  3. Don't init from the service constructor — initialization is owned by instrumentation.ts / instrumentation-client.ts. The service should be stateless.
  4. Provider enum — every new provider must be added to MONITORING_PROVIDERS in get-monitoring-provider.ts, otherwise Zod will reject the env var.
  5. Lazy loading — providers are loaded lazily through registries. Don't statically import provider SDKs from shared code, or you'll pull them into every bundle.
  6. Browser-only SDKs — providers like LogRocket can't run on the server. Either guard the server registration (return a no-op service) or skip the registration entirely.

This monitoring system is part of the Next.js Supabase SaaS Kit.


Previous: Sentry Configuration ←