Creating a Custom Monitoring Provider

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

How to create a custom monitoring provider

Add your preferred monitoring service to the kit.

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

Create a new package or add to the existing monitoring packages:

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

import LogRocket from 'logrocket';
import { MonitoringService } from '@kit/monitoring-core';
export class LogRocketMonitoringService implements MonitoringService {
private readonly readyPromise: Promise<unknown>;
private readyResolver?: (value?: unknown) => void;
constructor() {
this.readyPromise = new Promise(
(resolve) => (this.readyResolver = resolve),
);
void this.initialize();
}
async ready() {
return this.readyPromise;
}
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,
});
}
private async initialize() {
const appId = process.env.NEXT_PUBLIC_LOGROCKET_APP_ID;
if (!appId) {
console.warn('LogRocket app ID not configured');
this.readyResolver?.();
return;
}
if (typeof window !== 'undefined') {
LogRocket.init(appId);
}
this.readyResolver?.();
}
}

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

Client-Side Registration

Update the monitoring provider registry:

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

import { lazy } from 'react';
import { createRegistry } from '@kit/shared/registry';
import {
MonitoringProvider as MonitoringProviderType,
getMonitoringProvider,
} from '../get-monitoring-provider';
type ProviderComponent = {
default: React.ComponentType<React.PropsWithChildren>;
};
const provider = getMonitoringProvider();
const Provider = provider
? lazy(() => monitoringProviderRegistry.get(provider))
: null;
const monitoringProviderRegistry = createRegistry<
ProviderComponent,
NonNullable<MonitoringProviderType>
>();
// Existing Sentry registration
monitoringProviderRegistry.register('sentry', async () => {
const { SentryProvider } = await import('@kit/sentry/provider');
return {
default: function SentryProviderWrapper({ children }) {
return <SentryProvider>{children}</SentryProvider>;
},
};
});
// Add LogRocket registration
monitoringProviderRegistry.register('logrocket', async () => {
const { LogRocketProvider } = await import('@kit/logrocket/provider');
return {
default: function LogRocketProviderWrapper({ children }) {
return <LogRocketProvider>{children}</LogRocketProvider>;
},
};
});

Add Provider Type

Update the provider enum:

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

import * as z from 'zod';
const MONITORING_PROVIDERS = [
'sentry',
'logrocket', // Add your provider
'',
] as const;
export const MONITORING_PROVIDER = z
.enum(MONITORING_PROVIDERS)
.optional()
.transform((value) => value || undefined);
export type MonitoringProvider = z.output<typeof MONITORING_PROVIDER>;
export function getMonitoringProvider() {
return MONITORING_PROVIDER.parse(process.env.NEXT_PUBLIC_MONITORING_PROVIDER);
}

Create the Provider Component

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>
);
}

Server-Side Configuration

Register the provider for server-side error capture:

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

import {
ConsoleMonitoringService,
MonitoringService,
} from '@kit/monitoring-core';
import { createRegistry } from '@kit/shared/registry';
import {
MonitoringProvider,
getMonitoringProvider,
} from '../get-monitoring-provider';
const serverMonitoringRegistry = createRegistry<
MonitoringService,
NonNullable<MonitoringProvider>
>();
// Existing Sentry registration
serverMonitoringRegistry.register('sentry', async () => {
const { SentryMonitoringService } = await import('@kit/sentry');
return new SentryMonitoringService();
});
// Add LogRocket registration
serverMonitoringRegistry.register('logrocket', async () => {
const { LogRocketMonitoringService } = await import('@kit/logrocket');
return new LogRocketMonitoringService();
});
export async function getServerMonitoringService() {
const provider = getMonitoringProvider();
if (!provider) {
return new ConsoleMonitoringService();
}
return serverMonitoringRegistry.get(provider);
}

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:

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 {
private readonly readyPromise: Promise<unknown>;
private readyResolver?: (value?: unknown) => void;
constructor() {
this.readyPromise = new Promise(
(resolve) => (this.readyResolver = resolve),
);
void this.initialize();
}
async ready() {
return this.readyPromise;
}
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,
});
}
private async initialize() {
if (typeof window === 'undefined') {
this.readyResolver?.();
return;
}
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,
trackLongTasks: true,
});
this.readyResolver?.();
}
}

Common Gotchas

  1. Browser-only initialization - Check typeof window !== 'undefined' before accessing browser APIs.
  2. Ready state - The ready() method must resolve after initialization completes. Server contexts call await service.ready() before capturing.
  3. Provider enum - Remember to add your provider to the MONITORING_PROVIDERS array in get-monitoring-provider.ts.
  4. Lazy loading - Providers are loaded lazily through the registry. Don't import the monitoring service directly in your main bundle.
  5. Server vs client - Some providers (like LogRocket) are browser-only. Return a no-op or console fallback for server contexts.

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


Previous: Sentry Configuration ←