Next.js Supabase V3 (#463)
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
This commit is contained in:
committed by
GitHub
parent
4912e402a3
commit
7ebff31475
334
docs/monitoring/custom-monitoring-provider.mdoc
Normal file
334
docs/monitoring/custom-monitoring-provider.mdoc
Normal file
@@ -0,0 +1,334 @@
|
||||
---
|
||||
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)
|
||||
Reference in New Issue
Block a user