Back to recipes

Sentry Setup

Configure Sentry for error tracking, performance monitoring, and log aggregation. Integrates with Pino to forward logs to Sentry automatically.

Setup Instruction Resources

Step 1: Run the Sentry Wizard

Create a new Sentry project at sentry.io, then configure your app automatically by running the Sentry wizard in your project root. You can find the personalized command in the Sentry getting-started guide during project creation.

bash
bunx @sentry/wizard@latest -i nextjs --saas --org <org-name> --project <project-name>

Wizard selections:

  • Runtime: Bun
  • Route requests through your Next.js server: Yes (optional, recommended for privacy)
  • Enable Tracing: Yes
  • Session Replay: Yes
  • Logs: Yes
  • Example page: No
  • Add Sentry MCP server: Yes

The wizard creates and updates the following TypeScript files:

  • next.config.ts - Next.js configuration
  • sentry.server.config.ts - Server-side initialization (we'll delete this)
  • sentry.edge.config.ts - Edge runtime initialization (we'll delete this)
  • src/instrumentation-client.ts - Client-side initialization
  • src/instrumentation.ts - Instrumentation hook
  • src/app/global-error.tsx - Global error component

Step 2: Add environment variables

Add to your .env.development:

env
NEXT_PUBLIC_ENABLE_SENTRY="true"
NEXT_PUBLIC_SENTRY_DSN="https://your-dsn@sentry.io/your-project-id"
NEXT_PUBLIC_SENTRY_PROJECT="your-project-name"
NEXT_PUBLIC_SENTRY_ORG="your-org-name"
SENTRY_AUTH_TOKEN="your-auth-token"

Then sync to Vercel with bun run env:push.

You can find your Sentry DSN, project name, and org name in your Sentry project settings or within the scaffolded files generated by the Sentry wizard. They're not secrets - they just tell Sentry where to send data. We move these to .env.development to enforce validation and also avoid committing them when working on open source repositories.

SENTRY_AUTH_TOKEN is a secret token added in .env.sentry-build-plugin by the Sentry wizard. You can delete the .env.sentry-build-plugin file after adding the token to .env.development. After that, you can also revert the changes made to .gitignore by removing the .env.sentry-build-plugin line.


Step 3: Create the Sentry config

Create the Sentry config with environment variable validation:

ts
import { loadConfig } from "../common/load-config";

export const sentryConfig = loadConfig({
  name: "Sentry",
  flag: process.env.NEXT_PUBLIC_ENABLE_SENTRY,
  server: {
    // SENTRY_AUTH_TOKEN is picked up by the Sentry Build Plugin for source maps upload.
    // Accessing this on the client will throw ServerConfigClientAccessError.
    token: process.env.SENTRY_AUTH_TOKEN,
  },
  public: {
    dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
    project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,
    org: process.env.NEXT_PUBLIC_SENTRY_ORG,
  },
});

We use the loadConfig utility to validate the configuration and throw an error if any of the required environment variables are missing. The config separates server and public sections:

  • server.* values are only accessible on the server - accessing them on the client throws a helpful error
  • public.* values work everywhere (Next.js inlines NEXT_PUBLIC_* vars at build time)

Step 4: Create the initialization helpers

Next, we'll refactor the wizard-generated files to use the new Sentry config. We further move the Sentry-related code into the src/lib/sentry directory to keep them organized.

Create the server initialization helper:

ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";

export function initSentryServer() {
  if (!sentryConfig.isEnabled) return;

  Sentry.init({
    dsn: sentryConfig.public.dsn,
    tracesSampleRate: 1,
    enableLogs: true,
    sendDefaultPii: true,
    integrations: [
      Sentry.pinoIntegration({ log: { levels: ["info", "warn", "error"] } }),
    ],
  });
}

Create the edge initialization helper:

ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";

export function initSentryEdge() {
  if (!sentryConfig.isEnabled) return;

  Sentry.init({
    dsn: sentryConfig.public.dsn,
    tracesSampleRate: 1,
    enableLogs: true,
    sendDefaultPii: true,
  });
}

Note: The pino integration is not included for edge because pino uses Node.js modules (fs, events, worker_threads) that aren't available in the edge runtime.

Create the client initialization helper:

ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./config";

export function initSentryClient() {
  if (!sentryConfig.isEnabled) return;

  Sentry.init({
    dsn: sentryConfig.public.dsn,
    integrations: [Sentry.replayIntegration()],
    tracesSampleRate: 1,
    enableLogs: true,
    replaysSessionSampleRate: 0.1,
    replaysOnErrorSampleRate: 1.0,
    sendDefaultPii: true,
  });
}

export const onRouterTransitionStart = sentryConfig.isEnabled
  ? Sentry.captureRouterTransitionStart
  : () => {};

Step 5: Update the wizard-generated files

Delete the wizard-generated sentry.server.config.ts and sentry.edge.config.ts files from the project root. We'll import directly from our src/lib/sentry/ modules instead.

Replace the instrumentation file:

ts
import * as Sentry from "@sentry/nextjs";
import { sentryConfig } from "./lib/sentry/config";

export async function register() {
  if (process.env.NEXT_RUNTIME === "nodejs") {
    const { initSentryServer } = await import("./lib/sentry/server");
    initSentryServer();
  }

  if (process.env.NEXT_RUNTIME === "edge") {
    const { initSentryEdge } = await import("./lib/sentry/edge");
    initSentryEdge();
  }
}

export const onRequestError = sentryConfig.isEnabled
  ? Sentry.captureRequestError
  : undefined;

Replace the client instrumentation file:

ts
// This file configures the initialization of Sentry on the client.
// The added config here will be used whenever a users loads a page in their browser.
// https://docs.sentry.io/platforms/javascript/guides/nextjs/

import { initSentryClient, onRouterTransitionStart } from "./lib/sentry/client";

initSentryClient();

export { onRouterTransitionStart };

Step 6: Update next.config.ts

The Sentry wizard updates next.config.ts with hardcoded org and project values. Replace them with environment variables:

typescript
// next.config.ts
import type { NextConfig } from "next";
import { withSentryConfig } from "@sentry/nextjs";

const nextConfig: NextConfig = {
  /* config options here */
};

export default withSentryConfig(nextConfig, {
  // For all available options, see:
  // https://www.npmjs.com/package/@sentry/webpack-plugin#options

  org: process.env.NEXT_PUBLIC_SENTRY_ORG,
  project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,

  // Only print logs for uploading source maps in CI
  silent: !process.env.CI,

  // For all available options, see:
  // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/

  // Upload a larger set of source maps for prettier stack traces (increases build time)
  widenClientFileUpload: true,

  // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.
  // This can increase your server load as well as your hosting bill.
  // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-
  // side errors will fail.
  tunnelRoute: "/monitoring",

  webpack: {
    // Automatically tree-shake Sentry logger statements to reduce bundle size
    treeshake: {
      removeDebugLogging: true,
    },
    // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)
    // See the following for more information:
    // https://docs.sentry.io/product/crons/
    // https://vercel.com/docs/cron-jobs
    automaticVercelMonitors: true,
  },
});

Database query monitoring

When tracing is enabled (tracesSampleRate > 0), Sentry automatically instruments database queries using postgresIntegration. This is included by default - no additional configuration needed.

What you get automatically:

  • All pg (node-postgres) queries captured as spans
  • Query timing and slow query detection
  • Database performance visible in Sentry's Performance tab
  • Works with Drizzle ORM since it uses node-postgres under the hood

This uses OpenTelemetry instrumentation (@opentelemetry/instrumentation-pg) to hook into the pg library. Supports pg versions 8.x.

To disable or customize (if needed):

typescript
Sentry.init({
  dsn: "...",
  tracesSampleRate: 1,
  integrations: (defaults) => {
    // Remove postgres integration
    return defaults.filter((i) => i.name !== "Postgres");
  },
});

Step 7: Create Cursor rules for Sentry

Create .cursor/rules/sentry.mdc to help AI coding agents use Sentry APIs correctly:

markdown
---
description: Sentry error monitoring and tracking
alwaysApply: false
---

These examples should be used as guidance when configuring Sentry functionality within a project.

# Exception Catching

Use `Sentry.captureException(error)` to capture an exception and log the error in Sentry.

Use this in try catch blocks or areas where exceptions are expected

# Tracing Examples

Spans should be created for meaningful actions within an applications like button clicks, API calls, and function calls

Use the `Sentry.startSpan` function to create a span

Child spans can exist within a parent span

## Custom Span instrumentation in component actions

The `name` and `op` properties should be meaninful for the activities in the call.

Attach attributes based on relevant information and metrics from the request

```tsx
function TestComponent() {
  const handleTestButtonClick = () => {
    // Create a transaction/span to measure performance
    Sentry.startSpan(
      {
        op: "ui.click",
        name: "Test Button Click",
      },
      (span) => {
        const value = "some config";
        const metric = "some metric";
        // Metrics can be added to the span
        span.setAttribute("config", value);
        span.setAttribute("metric", metric);
        doSomething();
      },
    );
  };
  return (
    <button type="button" onClick={handleTestButtonClick}>
      Test Sentry
    </button>
  );
}
```

## Custom span instrumentation in API calls

The `name` and `op` properties should be meaninful for the activities in the call.

Attach attributes based on relevant information and metrics from the request

```typescript
async function fetchUserData(userId) {
  return Sentry.startSpan(
    {
      op: "http.client",
      name: `GET /api/users/${userId}`,
    },
    async () => {
      const response = await fetch(`/api/users/${userId}`);
      const data = await response.json();
      return data;
    },
  );
}
```

# Logs

Where logs are used, ensure Sentry is imported using `import * as Sentry from "@sentry/nextjs"`

Enable logging in Sentry using `Sentry.init({  enableLogs: true })`

Reference the logger using `const { logger } = Sentry`

Sentry offers a consoleLoggingIntegration that can be used to log specific console error types automatically without instrumenting the individual logger calls

## Configuration

In NextJS the client side Sentry initialization is in `instrumentation-client.(js|ts)`, the server initialization is in `sentry.server.config.ts` and the edge initialization is in `sentry.edge.config.ts`

Initialization does not need to be repeated in other files, it only needs to happen the files mentioned above. You should use `import * as Sentry from "@sentry/nextjs"` to reference Sentry functionality

### Baseline

```typescript
import * as Sentry from "@sentry/nextjs";

Sentry.init({
  dsn: "https://your-dsn@sentry.io/your-project-id",
  enableLogs: true,
});
```

### Logger Integration

```typescript
Sentry.init({
  dsn: "https://your-dsn@sentry.io/your-project-id",
  integrations: [
    // send console.log, console.warn, and console.error calls as logs to Sentry
    Sentry.consoleLoggingIntegration({ levels: ["log", "warn", "error"] }),
  ],
});
```

## Logger Examples

`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.

```typescript
logger.trace("Starting database connection", { database: "users" });
logger.debug(logger.fmt`Cache miss for user: ${userId}`);
logger.info("Updated profile", { profileId: 345 });
logger.warn("Rate limit reached for endpoint", {
  endpoint: "/api/results/",
  isEnterprise: false,
});
logger.error("Failed to process payment", {
  orderId: "order_123",
  amount: 99.99,
});
logger.fatal("Database connection pool exhausted", {
  database: "users",
  activeConnections: 100,
});
```

File Structure

After setup, you'll have these Sentry-related files:

src/
  instrumentation.ts           # Next.js instrumentation hook - calls initSentryServer() or initSentryEdge()
  instrumentation-client.ts    # Client instrumentation - calls initSentryClient()
  lib/sentry/
    config.ts                  # Validates NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_ORG
    server.ts                  # initSentryServer() with pino integration
    edge.ts                    # initSentryEdge() without pino (edge doesn't support Node.js modules)
    client.ts                  # initSentryClient() with replay integration
.cursor/rules/
  sentry.mdc                   # AI agent guidelines for Sentry

References