Skip to content

Observability

Lazy-loaded Sentry integration with global error capture and custom error reporting.

What Problem This Solves

You want error tracking in production without bloating your bundle with Sentry in development. This pattern lazy-loads Sentry only when VITE_SENTRY_DSN is present.

Prerequisites

  • Optional: @sentry/react

Files

observability.ts

ts
type ErrorCaptureFunction = (error: unknown, context?: string) => void;

let captureErrorFn: ErrorCaptureFunction = (error, context) => {
  if (import.meta.env.DEV) {
    const label = context ? `[${context}]` : "[error]";
    console.error(label, error);
  }
};

export const setErrorCaptureFunction = (fn: ErrorCaptureFunction) => {
  captureErrorFn = fn;
};

export const captureError: ErrorCaptureFunction = (error, context) => {
  captureErrorFn(error, context);
};

export const initObservability = async ({ sentryDsn }: { sentryDsn?: string }) => {
  // Global error handlers (always active)
  window.addEventListener("error", (event) => {
    captureError(event.error, "window.error");
  });

  window.addEventListener("unhandledrejection", (event) => {
    captureError(event.reason, "window.unhandledrejection");
  });

  // Lazy-load Sentry only if DSN provided
  if (sentryDsn) {
    try {
      const Sentry = await import("@sentry/react");
      Sentry.init({
        dsn: sentryDsn,
        tracesSampleRate: 0.1,
        environment: import.meta.env.MODE,
      });

      captureErrorFn = (error, context) => {
        Sentry.captureException(error, context ? { tags: { context } } : undefined);
      };
    } catch {
      console.warn("Failed to initialize Sentry");
    }
  }
};

Usage

1. Initialize on app boot

ts
// main.tsx
import { initObservability } from "./observability";

initObservability({
  sentryDsn: import.meta.env.VITE_SENTRY_DSN,
});

2. Capture errors manually

tsx
import { captureError } from "./observability";

async function riskyOperation() {
  try {
    await fetchData();
  } catch (error) {
    captureError(error, "DataFetch");
    // Show user-friendly error...
  }
}

3. Use with Error Boundary

tsx
import { captureError } from "./observability";

export function ErrorFallback({ error }: { error: Error }) {
  useEffect(() => {
    captureError(error, "ErrorBoundary");
  }, [error]);

  return <div>Something went wrong.</div>;
}

How It Works

  1. Always on: Global window.error and unhandledrejection listeners capture all uncaught errors
  2. Dev mode: Errors are logged to console with context labels
  3. Production without Sentry: Errors are silently captured but not sent anywhere (you can add your own setErrorCaptureFunction)
  4. Production with Sentry: Sentry is dynamically imported, keeping it out of the initial bundle

Custom Backend

If you don't use Sentry, provide your own capture function:

ts
import { setErrorCaptureFunction } from "./observability";

setErrorCaptureFunction((error, context) => {
  fetch("/api/errors", {
    method: "POST",
    body: JSON.stringify({
      message: error instanceof Error ? error.message : String(error),
      context,
      stack: error instanceof Error ? error.stack : undefined,
    }),
  });
});