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