Version 3 of the kit: - Radix UI replaced with Base UI (using the Shadcn UI patterns) - next-intl replaces react-i18next - enhanceAction deprecated; usage moved to next-safe-action - main layout now wrapped with [locale] path segment - Teams only mode - Layout updates - Zod v4 - Next.js 16.2 - Typescript 6 - All other dependencies updated - Removed deprecated Edge CSRF - Dynamic Github Action runner
334 lines
9.0 KiB
Plaintext
334 lines
9.0 KiB
Plaintext
---
|
|
status: "published"
|
|
title: 'Creating a Custom Monitoring Provider'
|
|
label: 'Custom Provider'
|
|
description: 'Integrate LogRocket, Bugsnag, Datadog, or any monitoring service by implementing the MonitoringService interface.'
|
|
order: 1
|
|
---
|
|
|
|
{% sequence title="How to create a custom monitoring provider" description="Add your preferred monitoring service to the kit." %}
|
|
|
|
[Implement the MonitoringService interface](#implement-the-monitoringservice-interface)
|
|
|
|
[Register the provider](#register-the-provider)
|
|
|
|
[Configure for server-side](#server-side-configuration)
|
|
|
|
{% /sequence %}
|
|
|
|
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:
|
|
|
|
```typescript {% title="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:
|
|
|
|
```json {% title="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"
|
|
}
|
|
}
|
|
```
|
|
|
|
```typescript {% title="packages/monitoring/logrocket/src/index.ts" %}
|
|
export { LogRocketMonitoringService } from './logrocket-monitoring.service';
|
|
```
|
|
|
|
## Register the Provider
|
|
|
|
### Client-Side Registration
|
|
|
|
Update the monitoring provider registry:
|
|
|
|
```typescript {% title="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:
|
|
|
|
```typescript {% title="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() {
|
|
const result = MONITORING_PROVIDER.safeParse(process.env.NEXT_PUBLIC_MONITORING_PROVIDER);
|
|
|
|
if (result.success) {
|
|
return result.data;
|
|
}
|
|
|
|
return undefined;
|
|
}
|
|
```
|
|
|
|
### Create the Provider Component
|
|
|
|
```typescript {% title="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:
|
|
|
|
```typescript {% title="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:
|
|
|
|
```bash {% title="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:
|
|
|
|
```typescript {% title="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 Supabase SaaS Kit](/next-supabase-turbo).
|
|
|
|
---
|
|
|
|
**Previous:** [Sentry Configuration ←](./sentry) |