# Creating a Custom Monitoring Provider

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

*Canonical: https://makerkit.dev/docs/tanstack-supabase/monitoring/custom-monitoring-provider*

---

{% 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 type](#register-the-provider-type)

[Register the React provider](#register-the-react-provider)

[Register the server service](#register-the-server-service)

[Register the server instrumentation](#register-the-server-instrumentation)

[Register the client instrumentation](#register-the-client-instrumentation)

{% /sequence %}

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

{% alert type="warning" title="Five registration points, not three" %}
SDK initialization is no longer owned by `MonitoringService`. The instrumentation files (`instrumentation.ts` and `instrumentation-client.ts`) own SDK init and provider-agnostic global error handlers; the service is a thin capture/identify wrapper. A custom provider has to register in **all five** registries below — in particular, the `onRequestError` entry is what keeps server errors from being silently dropped after the client error boundary's digest dedup.
{% /alert %}

## 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.

```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 {
  // 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:

```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 Type

Update the provider enum so `VITE_MONITORING_PROVIDER=logrocket` validates:

```typescript {% title="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:

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

```typescript {% title="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 functions, server routes, jobs):

```typescript {% title="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 Tanstack Start request errors. **`onRequestError` is required** — without it, every server error is dropped (the client error boundary already skips reports for errors with a `digest`).

```typescript {% title="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) => {
      const { getServerMonitoringService } = await import(
        '../services/get-server-monitoring-service'
      );

      const service = await getServerMonitoringService();

      await service.captureException(error as Error, {}, {
        url: request?.url,
        method: request?.method,
        headers: request?.headers,
      });
    },
  };
});
```

The `onRequestError` handler receives the web `Request` that failed. If your SDK provides a built-in request-error handler, 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.

```typescript {% title="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.VITE_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:

```bash {% title="apps/web/.env.local" %}
# Enable LogRocket as the monitoring provider
VITE_MONITORING_PROVIDER=logrocket

# LogRocket configuration
VITE_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.

```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 {
  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,
    });
  }
}
```

```typescript {% title="packages/monitoring/datadog/src/datadog.client.config.ts" %}
import { datadogRum } from '@datadog/browser-rum';

export function initializeDatadogBrowserClient() {
  datadogRum.init({
    applicationId: process.env.VITE_DATADOG_APP_ID!,
    clientToken: process.env.VITE_DATADOG_CLIENT_TOKEN!,
    site: process.env.VITE_DATADOG_SITE ?? 'datadoghq.com',
    service: process.env.VITE_DATADOG_SERVICE ?? 'my-saas',
    env: process.env.VITE_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 [Tanstack Start Supabase SaaS Kit](/next-supabase-turbo).

---

**Previous:** [Sentry Configuration ←](./sentry)
