{"content":"# Sentry Setup\n\nConfigure Sentry for error tracking, performance monitoring, and log aggregation. Integrates with Pino to forward logs to Sentry automatically.\n\n### Step 1: Run the Sentry Wizard\n\nCreate a new Sentry project at [sentry.io](https://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.\n\n```bash\nbunx @sentry/wizard@latest -i nextjs --saas --org <org-name> --project <project-name>\n```\n\n**Wizard selections:**\n\n- Runtime: **Bun**\n- Route requests through your Next.js server: **Yes** (optional, recommended for privacy)\n- Enable Tracing: **Yes**\n- Session Replay: **Yes**\n- Logs: **Yes**\n- Example page: **No**\n- Add Sentry MCP server: **Yes**\n\nThe wizard creates and updates the following TypeScript files:\n\n- `next.config.ts` - Next.js configuration\n- `sentry.server.config.ts` - Server-side initialization (we'll delete this)\n- `sentry.edge.config.ts` - Edge runtime initialization (we'll delete this)\n- `src/instrumentation-client.ts` - Client-side initialization\n- `src/instrumentation.ts` - Instrumentation hook\n- `src/app/global-error.tsx` - Global error component\n\n---\n\n### Step 2: Add environment variables\n\nAdd to your `.env.development`:\n\n```env\nNEXT_PUBLIC_ENABLE_SENTRY=\"true\"\nNEXT_PUBLIC_SENTRY_DSN=\"https://your-dsn@sentry.io/your-project-id\"\nNEXT_PUBLIC_SENTRY_PROJECT=\"your-project-name\"\nNEXT_PUBLIC_SENTRY_ORG=\"your-org-name\"\nSENTRY_AUTH_TOKEN=\"your-auth-token\"\n```\n\nThen sync to Vercel with `bun run env:push`.\n\nYou 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.\n\n`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.\n\n---\n\n### Step 3: Create the Sentry config\n\nCreate the Sentry config with environment variable validation:\n\n```typescript\n// src/lib/sentry/config.ts\nimport { configSchema, pub, server } from \"better-env/config-schema\";\n\nexport const sentryConfig = configSchema(\n  \"Sentry\",\n  {\n    // SENTRY_AUTH_TOKEN is picked up by the Sentry Build Plugin for source maps upload.\n    token: server({ env: \"SENTRY_AUTH_TOKEN\" }),\n    dsn: pub({\n      env: \"NEXT_PUBLIC_SENTRY_DSN\",\n      value: process.env.NEXT_PUBLIC_SENTRY_DSN,\n    }),\n    project: pub({\n      env: \"NEXT_PUBLIC_SENTRY_PROJECT\",\n      value: process.env.NEXT_PUBLIC_SENTRY_PROJECT,\n    }),\n    org: pub({\n      env: \"NEXT_PUBLIC_SENTRY_ORG\",\n      value: process.env.NEXT_PUBLIC_SENTRY_ORG,\n    }),\n  },\n  {\n    flag: {\n      env: \"NEXT_PUBLIC_ENABLE_SENTRY\",\n      value: process.env.NEXT_PUBLIC_ENABLE_SENTRY,\n    },\n  },\n);\n```\n\nWe use `better-env/config-schema` to validate the configuration and throw an error if any of the required environment variables are missing. The config separates `server` and `public` sections:\n\n- `server.*` values are only accessible on the server - accessing them on the client throws a helpful error\n- `public.*` values work everywhere (Next.js inlines `NEXT_PUBLIC_*` vars at build time)\n\n---\n\n### Step 4: Create the initialization helpers\n\nNext, 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.\n\nCreate the server initialization helper:\n\n```typescript\n// src/lib/sentry/server.ts\nimport * as Sentry from \"@sentry/nextjs\";\nimport { sentryConfig } from \"./config\";\n\nexport function initSentryServer() {\n  if (!sentryConfig.isEnabled) return;\n\n  Sentry.init({\n    dsn: sentryConfig.public.dsn,\n    tracesSampleRate: 1,\n    enableLogs: true,\n    sendDefaultPii: true,\n    integrations: [\n      Sentry.pinoIntegration({ log: { levels: [\"info\", \"warn\", \"error\"] } }),\n    ],\n  });\n}\n```\n\nCreate the edge initialization helper:\n\n```typescript\n// src/lib/sentry/edge.ts\nimport * as Sentry from \"@sentry/nextjs\";\nimport { sentryConfig } from \"./config\";\n\nexport function initSentryEdge() {\n  if (!sentryConfig.isEnabled) return;\n\n  Sentry.init({\n    dsn: sentryConfig.public.dsn,\n    tracesSampleRate: 1,\n    enableLogs: true,\n    sendDefaultPii: true,\n  });\n}\n```\n\nNote: 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.\n\nCreate the client initialization helper:\n\n```typescript\n// src/lib/sentry/client.ts\nimport * as Sentry from \"@sentry/nextjs\";\nimport { sentryConfig } from \"./config\";\n\nexport function initSentryClient() {\n  if (!sentryConfig.isEnabled) return;\n\n  Sentry.init({\n    dsn: sentryConfig.public.dsn,\n    integrations: [Sentry.replayIntegration()],\n    tracesSampleRate: 1,\n    enableLogs: true,\n    replaysSessionSampleRate: 0.1,\n    replaysOnErrorSampleRate: 1.0,\n    sendDefaultPii: true,\n  });\n}\n\nexport const onRouterTransitionStart = sentryConfig.isEnabled\n  ? Sentry.captureRouterTransitionStart\n  : () => {};\n```\n\n---\n\n### Step 5: Update the wizard-generated files\n\nDelete 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.\n\nReplace the instrumentation file:\n\n```typescript\n// src/instrumentation.ts\nimport * as Sentry from \"@sentry/nextjs\";\nimport { sentryConfig } from \"./lib/sentry/config\";\n\nexport async function register() {\n  if (process.env.NEXT_RUNTIME === \"nodejs\") {\n    const { initSentryServer } = await import(\"./lib/sentry/server\");\n    initSentryServer();\n  }\n\n  if (process.env.NEXT_RUNTIME === \"edge\") {\n    const { initSentryEdge } = await import(\"./lib/sentry/edge\");\n    initSentryEdge();\n  }\n}\n\nexport const onRequestError = sentryConfig.isEnabled\n  ? Sentry.captureRequestError\n  : undefined;\n```\n\nReplace the client instrumentation file:\n\n```typescript\n// src/instrumentation-client.ts\n// This file configures the initialization of Sentry on the client.\n// The added config here will be used whenever a users loads a page in their browser.\n// https://docs.sentry.io/platforms/javascript/guides/nextjs/\n\nimport { initSentryClient, onRouterTransitionStart } from \"./lib/sentry/client\";\n\ninitSentryClient();\n\nexport { onRouterTransitionStart };\n```\n\n---\n\n### Step 6: Update next.config.ts\n\nThe Sentry wizard updates `next.config.ts` with hardcoded org and project values. Replace them with environment variables:\n\n```typescript\n// next.config.ts\nimport type { NextConfig } from \"next\";\nimport { withSentryConfig } from \"@sentry/nextjs\";\n\nconst nextConfig: NextConfig = {\n  /* config options here */\n};\n\nexport default withSentryConfig(nextConfig, {\n  // For all available options, see:\n  // https://www.npmjs.com/package/@sentry/webpack-plugin#options\n\n  org: process.env.NEXT_PUBLIC_SENTRY_ORG,\n  project: process.env.NEXT_PUBLIC_SENTRY_PROJECT,\n\n  // Only print logs for uploading source maps in CI\n  silent: !process.env.CI,\n\n  // For all available options, see:\n  // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/\n\n  // Upload a larger set of source maps for prettier stack traces (increases build time)\n  widenClientFileUpload: true,\n\n  // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers.\n  // This can increase your server load as well as your hosting bill.\n  // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client-\n  // side errors will fail.\n  tunnelRoute: \"/monitoring\",\n\n  webpack: {\n    // Automatically tree-shake Sentry logger statements to reduce bundle size\n    treeshake: {\n      removeDebugLogging: true,\n    },\n    // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.)\n    // See the following for more information:\n    // https://docs.sentry.io/product/crons/\n    // https://vercel.com/docs/cron-jobs\n    automaticVercelMonitors: true,\n  },\n});\n```\n\n---\n\n### Database query monitoring\n\nWhen tracing is enabled (`tracesSampleRate > 0`), Sentry automatically instruments database queries using `postgresIntegration`. This is included by default - no additional configuration needed.\n\n**What you get automatically:**\n\n- All `pg` (node-postgres) queries captured as spans\n- Query timing and slow query detection\n- Database performance visible in Sentry's Performance tab\n- Works with Drizzle ORM since it uses `node-postgres` under the hood\n\nThis uses OpenTelemetry instrumentation (`@opentelemetry/instrumentation-pg`) to hook into the `pg` library. Supports `pg` versions 8.x.\n\n**To disable or customize** (if needed):\n\n```typescript\nSentry.init({\n  dsn: \"...\",\n  tracesSampleRate: 1,\n  integrations: (defaults) => {\n    // Remove postgres integration\n    return defaults.filter((i) => i.name !== \"Postgres\");\n  },\n});\n```\n\n---\n\n### Step 7: Create Cursor rules for Sentry\n\nCreate `.cursor/rules/sentry.mdc` to help AI coding agents use Sentry APIs correctly:\n\n```markdown\n---\ndescription: Sentry error monitoring and tracking\nalwaysApply: false\n---\n\nThese examples should be used as guidance when configuring Sentry functionality within a project.\n\n# Exception Catching\n\nUse `Sentry.captureException(error)` to capture an exception and log the error in Sentry.\n\nUse this in try catch blocks or areas where exceptions are expected\n\n# Tracing Examples\n\nSpans should be created for meaningful actions within an applications like button clicks, API calls, and function calls\n\nUse the `Sentry.startSpan` function to create a span\n\nChild spans can exist within a parent span\n\n## Custom Span instrumentation in component actions\n\nThe `name` and `op` properties should be meaninful for the activities in the call.\n\nAttach attributes based on relevant information and metrics from the request\n\n```tsx\nfunction TestComponent() {\n  const handleTestButtonClick = () => {\n    // Create a transaction/span to measure performance\n    Sentry.startSpan(\n      {\n        op: \"ui.click\",\n        name: \"Test Button Click\",\n      },\n      (span) => {\n        const value = \"some config\";\n        const metric = \"some metric\";\n        // Metrics can be added to the span\n        span.setAttribute(\"config\", value);\n        span.setAttribute(\"metric\", metric);\n        doSomething();\n      },\n    );\n  };\n  return (\n    <button type=\"button\" onClick={handleTestButtonClick}>\n      Test Sentry\n    </button>\n  );\n}\n```\n\n## Custom span instrumentation in API calls\n\nThe `name` and `op` properties should be meaninful for the activities in the call.\n\nAttach attributes based on relevant information and metrics from the request\n\n```typescript\nasync function fetchUserData(userId) {\n  return Sentry.startSpan(\n    {\n      op: \"http.client\",\n      name: `GET /api/users/${userId}`,\n    },\n    async () => {\n      const response = await fetch(`/api/users/${userId}`);\n      const data = await response.json();\n      return data;\n    },\n  );\n}\n```\n\n# Logs\n\nWhere logs are used, ensure Sentry is imported using `import * as Sentry from \"@sentry/nextjs\"`\n\nEnable logging in Sentry using `Sentry.init({  enableLogs: true })`\n\nReference the logger using `const { logger } = Sentry`\n\nSentry offers a consoleLoggingIntegration that can be used to log specific console error types automatically without instrumenting the individual logger calls\n\n## Configuration\n\nIn 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`\n\nInitialization 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\n\n### Baseline\n\n```typescript\nimport * as Sentry from \"@sentry/nextjs\";\n\nSentry.init({\n  dsn: \"https://your-dsn@sentry.io/your-project-id\",\n  enableLogs: true,\n});\n```\n\n### Logger Integration\n\n```typescript\nSentry.init({\n  dsn: \"https://your-dsn@sentry.io/your-project-id\",\n  integrations: [\n    // send console.log, console.warn, and console.error calls as logs to Sentry\n    Sentry.consoleLoggingIntegration({ levels: [\"log\", \"warn\", \"error\"] }),\n  ],\n});\n```\n\n## Logger Examples\n\n`logger.fmt` is a template literal function that should be used to bring variables into the structured logs.\n\n```typescript\nlogger.trace(\"Starting database connection\", { database: \"users\" });\nlogger.debug(logger.fmt`Cache miss for user: ${userId}`);\nlogger.info(\"Updated profile\", { profileId: 345 });\nlogger.warn(\"Rate limit reached for endpoint\", {\n  endpoint: \"/api/results/\",\n  isEnterprise: false,\n});\nlogger.error(\"Failed to process payment\", {\n  orderId: \"order_123\",\n  amount: 99.99,\n});\nlogger.fatal(\"Database connection pool exhausted\", {\n  database: \"users\",\n  activeConnections: 100,\n});\n```\n```\n\n---\n\n## File Structure\n\nAfter setup, you'll have these Sentry-related files:\n\n```\nsrc/\n  instrumentation.ts           # Next.js instrumentation hook - calls initSentryServer() or initSentryEdge()\n  instrumentation-client.ts    # Client instrumentation - calls initSentryClient()\n  lib/sentry/\n    config.ts                  # Validates NEXT_PUBLIC_SENTRY_DSN, NEXT_PUBLIC_SENTRY_PROJECT, NEXT_PUBLIC_SENTRY_ORG\n    server.ts                  # initSentryServer() with pino integration\n    edge.ts                    # initSentryEdge() without pino (edge doesn't support Node.js modules)\n    client.ts                  # initSentryClient() with replay integration\n.cursor/rules/\n  sentry.mdc                   # AI agent guidelines for Sentry\n```\n\n---\n\n## References\n\n- [Sentry Pino Integration](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/pino/)\n- [Sentry Postgres Integration](https://docs.sentry.io/platforms/javascript/guides/nextjs/configuration/integrations/postgres/)\n- [Sentry Next.js Guide](https://docs.sentry.io/platforms/javascript/guides/nextjs/)\n- [Sentry Performance Monitoring](https://docs.sentry.io/platforms/javascript/guides/nextjs/tracing/)"}