MemberPulse
IntegrationsMonitoring

Sentry Integration

Error monitoring and performance tracking with Sentry

MemberPulse uses Sentry for error monitoring, performance tracking, and session replay. This guide covers the integration setup for the TanStack Start + Convex + React Query stack.

Architecture Overview

The Sentry integration consists of three parts:

  1. Client-side SDK - Captures browser errors, performance traces, and session replays
  2. Server-side instrumentation - Captures server errors and traces
  3. React Query integration - Reports Convex query/mutation errors to Sentry

Sentry only auto-captures uncaught exceptions. Errors handled by React Query (including Convex errors) require explicit reporting via the QueryCache and MutationCache handlers.

Installation

bun add @sentry/tanstackstart-react @sentry/vite-plugin

Configuration

Client-Side Setup

Initialize Sentry in src/router.tsx:

import * as Sentry from "@sentry/tanstackstart-react";
import { createRouter } from "@tanstack/react-router";
import { QueryClient, QueryCache, MutationCache } from "@tanstack/react-query";
import { ConvexQueryClient } from "@convex-dev/react-query";
import { ConvexReactClient } from "convex/react";
import { routeTree } from "./routeTree.gen";

const convex = new ConvexReactClient(import.meta.env.VITE_CONVEX_URL as string);
const convexQueryClient = new ConvexQueryClient(convex);

const queryClient = new QueryClient({
  // Report React Query errors to Sentry
  queryCache: new QueryCache({
    onError: (error, query) => {
      Sentry.captureException(error, {
        tags: { type: "query" },
        extra: { queryKey: query.queryKey },
      });
    },
  }),
  mutationCache: new MutationCache({
    onError: (error, _variables, _context, mutation) => {
      Sentry.captureException(error, {
        tags: { type: "mutation" },
        extra: { mutationKey: mutation.options.mutationKey },
      });
    },
  }),
  defaultOptions: {
    queries: {
      queryKeyHashFn: convexQueryClient.hashFn(),
      queryFn: convexQueryClient.queryFn(),
    },
  },
});
convexQueryClient.connect(queryClient);

export const getRouter = () => {
  const router = createRouter({
    routeTree,
    context: { queryClient, convex },
    scrollRestoration: true,
    defaultPreloadStaleTime: 0,
  });

  if (!router.isServer) {
    Sentry.init({
      dsn: "YOUR_SENTRY_DSN",
      sendDefaultPii: true,
      integrations: [
        Sentry.tanstackRouterBrowserTracingIntegration(router),
        Sentry.replayIntegration(),
      ],
      tracesSampleRate: import.meta.env.DEV ? 1.0 : 0.2,
      replaysSessionSampleRate: 0.1,
      replaysOnErrorSampleRate: 1.0,
    });
  }

  return router;
};

Server-Side Setup

Create instrument.server.mjs in the project root:

import * as Sentry from "@sentry/tanstackstart-react";

Sentry.init({
  dsn: "YOUR_SENTRY_DSN",
  sendDefaultPii: true,
  tracesSampleRate: 1.0,
});

Create src/server.ts to wrap the server entry:

import { wrapFetchWithSentry } from "@sentry/tanstackstart-react";
import handler, { createServerEntry } from "@tanstack/react-start/server-entry";

export default createServerEntry(
  wrapFetchWithSentry({
    fetch(request: Request) {
      return handler.fetch(request);
    },
  }),
);

Vite Plugin

Add the Sentry plugin to vite.config.ts:

import { sentryTanstackStart } from "@sentry/tanstackstart-react";

export default defineConfig({
  plugins: [
    // ... other plugins
    sentryTanstackStart({
      org: "your-org",
      project: "your-project",
      authToken: process.env.SENTRY_AUTH_TOKEN,
    }),
  ],
});

Package Scripts

Update package.json to load the server instrumentation:

{
  "scripts": {
    "dev": "NODE_OPTIONS='--import ./instrument.server.mjs' vite dev --port 3000",
    "build": "vite build && cp instrument.server.mjs .output/server",
    "start": "node --import ./.output/server/instrument.server.mjs .output/server/index.mjs"
  }
}

Why React Query Errors Need Manual Reporting

Sentry auto-captures uncaught exceptions and unhandled promise rejections. However, React Query catches all errors internally and renders them as error states rather than throwing.

This means:

Error TypeAuto-captured by Sentry?
Uncaught throw new Error()Yes
Unhandled promise rejectionsYes
Convex query errors via useQueryNo
Convex mutation errorsNo

The QueryCache and MutationCache onError handlers bridge this gap by explicitly reporting caught errors to Sentry with relevant context.

What Gets Captured

With this setup, Sentry captures:

  • Uncaught exceptions - JavaScript errors that bubble up
  • Convex query errors - Auth failures, validation errors, server errors
  • Convex mutation errors - Write failures, conflicts
  • Performance traces - Page loads, route transitions
  • Session replays - Video-like recordings of user sessions with errors

Each error includes:

  • tags.type: "query" or "mutation"
  • extra.queryKey or extra.mutationKey: The operation that failed
  • Stack trace and source maps (in production builds)

Environment Variables

VariableDescription
SENTRY_AUTH_TOKENAuth token for source map uploads (build time only)
VITE_SENTRY_DSNOptional: DSN as env var instead of hardcoded

The SENTRY_AUTH_TOKEN should only be available at build time, not shipped to the client. The DSN is safe to include in client code.

Ad Blocker Considerations

Some ad blockers block requests to sentry.io. Options:

  1. Accept it - Most users (~85%) don't have aggressive ad blockers
  2. Tunnel - Proxy Sentry requests through your own domain (adds complexity)
  3. Self-hosted - Run your own Sentry instance (enterprise use case)

For most applications, direct mode without tunneling is sufficient.

On this page