Sentry Integration Patterns
Overview
Implement error tracking and performance monitoring using Sentry following the project's established patterns for both client and server.
When to Use This Skill
-
Configuring Sentry SDK
-
Adding error tracking to TRPC procedures
-
Implementing custom spans/traces
-
Setting up user context
-
Working with Sentry API
SDK Configuration
Server-side
// apps/web-app/src/server.ts import * as Sentry from "@sentry/tanstackstart-react";
Sentry.init({ ...(env.SENTRY_DSN && { dsn: env.SENTRY_DSN }), sendDefaultPii: true, environment: env.ENVIRONMENT, release: env.VERSION, dist: "server", spotlight: isDev, // Local dev debugging enableLogs: true, tracesSampleRate: isDev ? 1.0 : 0.001, profilesSampleRate: isDev ? 1.0 : 0.001, profileLifecycle: "trace", integrations: [Sentry.postgresIntegration(), Sentry.redisIntegration(), Sentry.httpIntegration()], ignoreTransactions: ["/api/alive", "/api/health"], beforeSend(event, hint) { // Filter AbortError completely if (error instanceof DOMException && error.name === "AbortError") { return null; } // Mark expected TRPC errors as handled return markExpectedTRPCErrorInEvent(event, error); }, });
Client-side
// apps/web-app/src/client.tsx Sentry.init({ ...(env.VITE_SENTRY_DSN && { dsn: env.VITE_SENTRY_DSN }), sendDefaultPii: true, environment: env.VITE_ENVIRONMENT, release: env.VITE_VERSION, dist: "client", spotlight: isDev, integrations: [ Sentry.tanstackRouterBrowserTracingIntegration(router), Sentry.replayIntegration(), Sentry.feedbackIntegration({ colorScheme: "system", autoInject: false, }), ...(isDev ? [Sentry.spotlightBrowserIntegration()] : []), ], tracesSampleRate: isDev ? 1.0 : 0.001, replaysSessionSampleRate: 0.1, replaysOnErrorSampleRate: 1.0, beforeSend(event, hint) { return markExpectedTRPCErrorInEvent(event, hint.originalException); }, });
TRPC Middleware
// apps/web-app/src/infrastructure/trpc/init.ts export const sentryMiddleware = t.middleware( Sentry.trpcMiddleware({ attachRpcInput: true, }), );
// Used in procedure chain export const publicProcedure = t.procedure.use(debugMiddleware).use(sentryMiddleware);
Server Function Middleware
// apps/web-app/src/infrastructure/middleware/sentry-function-middleware.ts export const sentryFunctionMiddleware = createMiddleware({ type: "function", }) .client(async ({ next, serverFnMeta }) => { // Client-side timing and error logging }) .server(async ({ next, data, serverFnMeta }) => { return Sentry.startSpan( { op: "function.server", name: functionName, attributes: { hasData: data !== undefined, functionName, }, }, async (span) => { try { const result = await next(); span.setStatus({ code: 1 }); // OK return result; } catch (error) { captureException(error); span.setStatus({ code: 2 }); // ERROR throw error; } }, ); });
Error Capture Patterns
Expected TRPC Error Codes
// From apps/web-app/src/infrastructure/errors.ts const EXPECTED_TRPC_CODES = ["NOT_FOUND", "FORBIDDEN", "UNAUTHORIZED", "BAD_REQUEST"];
Custom Capture Helper
// apps/web-app/src/infrastructure/sentry-utils.ts export function captureException(error: unknown, captureContext?: Sentry.CaptureContext) { if (isExpectedTRPCError(error)) { Sentry.withScope((scope) => { scope.setLevel("warning"); scope.setTag("error.expected", "true"); Sentry.captureException(error, { mechanism: { type: "generic", handled: true }, }); }); } else { Sentry.captureException(error, captureContext); } }
beforeSend Helper
export function markExpectedTRPCErrorInEvent(event: Sentry.ErrorEvent, error: unknown) { if (!isExpectedTRPCError(error)) return event;
event.exception?.values?.forEach((exception) => { exception.mechanism = { type: "generic", handled: true, }; }); event.level = "warning"; event.tags = { ...event.tags, "error.expected": "true" }; return event; }
Database Query Tracing
Manual Tracing
// apps/web-app/src/infrastructure/db/tracing.ts export async function traced<T>(query: DrizzleQuery, operationName?: string): Promise<T> { return Sentry.startSpan( { op: "db.query", name: sqlString, attributes: { "db.system": "postgresql" }, }, () => query as Promise<T>, ); }
// Usage: const users = await traced(db.select().from(usersTable).where(eq(usersTable.id, userId)));
Automatic Proxy Tracing
// apps/web-app/src/infrastructure/db/traced-db.ts export function createTracedDb(db: Db): Db { return new Proxy(db, { get(target, prop) { if (["select", "insert", "update", "delete"].includes(String(prop))) { return function (...args) { const queryBuilder = originalMethod.call(target, ...args); return createTracedQueryBuilder(queryBuilder, String(prop)); }; } return originalMethod; }, }); }
User Context Hook
// apps/web-app/src/hooks/use-set-sentry-context.ts export function useSetSentryContext() { const { session } = Route.useRouteContext();
useLayoutEffect(() => { if (session?.user) { Sentry.setUser({ id: session.user.id, email: session.user.email, username: session.user.name, }); } else { Sentry.setUser(null); } }, [session]); }
Sentry API Service (Effect-based)
Error Types
// packages/services/src/sentry/errors.ts export class SentryApiError extends Schema.TaggedError<SentryApiError>()("SentryApiError", { statusCode: Schema.Number, body: Schema.String, url: Schema.String, }) {}
export class SentryRateLimitError extends Schema.TaggedError<SentryRateLimitError>()( "SentryRateLimitError", { retryAfter: Schema.Number, message: Schema.String, }, ) {}
Service Pattern
// packages/services/src/sentry/sentry-issues.ts export class SentryIssuesService extends Context.Tag("@project/SentryIssuesService")< SentryIssuesService, { readonly getNewErrors: (params) => Effect.Effect<SentryIssuesResponse, SentryError>; readonly getRegressions: (params) => Effect.Effect<SentryIssuesResponse, SentryError>; readonly verifyToken: (params) => Effect.Effect<SentryOrganization[], SentryError>; readonly listProjects: (params) => Effect.Effect<SentryProject[], SentryError>; }
() { static readonly layer = Layer.effect( SentryIssuesService, Effect.gen(function* () { const httpClient = yield* HttpClient.HttpClient; // ... service implementation }), ); }
// Live layer export const SentryIssuesServiceLive = SentryIssuesService.layer.pipe( Layer.provide(FetchHttpClient.layer), );
API Client with Retry
const retryPolicy = Schedule.exponential("500 millis").pipe(Schedule.compose(Schedule.recurs(2)));
export const sentryRequest = <A, I>( httpClient: HttpClient.HttpClient, schema: Schema.Schema<A, I>, params: SentryRequestParams, ): Effect.Effect<{ data: A; nextCursor: string | null }, SentryApiError | SentryRateLimitError> => Effect.fn("Sentry.request")(function* () { // Request with Bearer token auth // Rate limit handling (429 -> SentryRateLimitError) // Schema validation // Link header pagination parsing })();
Key Rules
-
Use beforeSend to mark expected errors as handled
-
Filter health check endpoints from transactions
-
Set user context in layout effect for session changes
-
Use startSpan for custom instrumentation
-
Handle rate limits (429) with SentryRateLimitError
-
Different sample rates for dev vs production
-
Include replays for error sessions (100% on error)