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 VITE_MONITORING_PROVIDER environment variable. You can add support for LogRocket, Bugsnag, Datadog, or any other service.
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.
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 VITE_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 functions, server routes, 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 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).
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.
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:
apps/web/.env.local
# Enable LogRocket as the monitoring providerVITE_MONITORING_PROVIDER=logrocket# LogRocket configurationVITE_LOGROCKET_APP_ID=your-org/your-appExample: 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.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
- Don't double-install global handlers —
@kit/monitoring/instrumentation-clientownswindow.onerrorandunhandledrejection. Disable any equivalent feature in your SDK's options (Sentry'sGlobalHandlers, Datadog'sforwardErrorsToLogs, etc.). onRequestErroris required — the client error boundary skips reports for errors that carry adigest. Without a server-sideonRequestError, those errors disappear.- Don't init from the service constructor — initialization is owned by
instrumentation.ts/instrumentation-client.ts. The service should be stateless. - Provider enum — every new provider must be added to
MONITORING_PROVIDERSinget-monitoring-provider.ts, otherwise Zod will reject the env var. - 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.
- 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.
Previous: Sentry Configuration ←