Claude Code with Sentry: Error Tracking That Actually Helps
Why Sentry without CLAUDE.md produces unactionable errors
Sentry is the most mature error tracking and performance monitoring platform available to JavaScript developers in 2026. The SDK is small, the TypeScript types are accurate, and a basic install is one command. The problem is that Claude Code does not know which configuration options separate actionable error reports from noise that gets ignored. Without explicit constraints, Claude generates code that works in the sense that errors land in Sentry, but produces reports with minified stack traces, no release attribution, PII in event payloads, and performance sampling so high it blows through the quota in a day.
The most common Claude defaults that hurt observability: instantiating Sentry.init() multiple times across the codebase, omitting the source map upload step which leaves stack traces as one-letter variable soup, hardcoding the DSN instead of reading from environment variables, missing the release identifier which makes "did the fix work" impossible to answer, setting tracesSampleRate: 1.0 and burning through the Sentry quota, and including PII like email addresses and IP addresses in event payloads without explicit consent. None of these surface as TypeScript errors because the Sentry SDK accepts every configuration silently.
This guide covers the CLAUDE.md configuration that locks Claude Code into Sentry's correct model: a single init at the framework entry point, source maps uploaded as part of the build, release tagging from CI, sampling that respects the quota, and PII scrubbing that complies with GDPR and CCPA. If you are running on Vercel, Claude Code with Vercel covers the build hook that handles source map upload automatically. For Next.js apps specifically, Claude Code with Next.js shows the App Router integration pattern.
The Sentry CLAUDE.md template
The CLAUDE.md at your project root is read at the start of every Claude Code session. For a Sentry integration it needs to declare: the SDK version, the init location, the source map upload pipeline, the DSN policy, the sampling targets, the PII rules, and the hard rules that block the mistakes Claude makes most often.
# Sentry error tracking rules
## Stack
- @sentry/nextjs ^8.x (or @sentry/node, @sentry/react as appropriate)
- TypeScript 5.x strict
- Source maps uploaded via Sentry CLI during build
- DSN stored in NEXT_PUBLIC_SENTRY_DSN (.env) - safe to expose, scoped to project
## Init location (CRITICAL)
- Next.js: sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts
- Node.js: top of entry file, BEFORE any other imports
- React (CRA / Vite): top of src/index.tsx or src/main.tsx, BEFORE ReactDOM.render
- NEVER call Sentry.init() more than once per runtime
- NEVER call Sentry.init() conditionally based on env (always init, control via DSN)
## Source maps (MANDATORY for production)
- sentry-cli sourcemaps upload runs in CI after build
- Or @sentry/webpack-plugin / @sentry/vite-plugin for build-time upload
- Without source maps, stack traces show minified variable names
- Verify upload: Sentry Releases page shows the source map artifacts attached
## Release identifier
- Every event MUST be tagged with release: SENTRY_RELEASE env var
- CI sets SENTRY_RELEASE to git commit SHA or semver tag
- Init reads it: release: process.env.SENTRY_RELEASE
- Sentry CLI creates the release before deploy: sentry-cli releases new $SENTRY_RELEASE
## Sampling
- tracesSampleRate: production 0.1 (10%), staging 1.0, dev 0
- replaysSessionSampleRate: production 0.0 - 0.1
- replaysOnErrorSampleRate: 1.0 (always replay errors)
- NEVER set tracesSampleRate to 1.0 in production (quota burn)
## PII (GDPR / CCPA)
- sendDefaultPii: false (default, KEEP it)
- beforeSend hook scrubs: email, phone, full IP, auth tokens
- For known-PII events (signup, login), wrap in withScope and scrub manually
- NEVER tag events with raw user email, use user.id only
## Hard rules
- NEVER hardcode the DSN in source files
- NEVER call Sentry.init() outside the framework-specific init file
- NEVER set tracesSampleRate: 1.0 in production
- NEVER skip source map upload
- NEVER tag events with raw email/IP/auth tokens
- ALWAYS set the release env var in CI
- ALWAYS use Sentry.captureException(error) for caught errors, NOT console.error
Three rules here prevent the majority of production issues Claude generates without them.
The single init rule prevents a class of bugs where multiple Sentry.init() calls in different files compete for global state. The last init wins, but middleware and scope behaviour gets unpredictable. The Next.js SDK enforces three init files (client, server, edge) and that is the only correct pattern. Claude tends to add init calls to API routes "to make sure errors are caught", which silently overrides the entry init.
The source maps rule is the most impactful for actionable errors. Production builds minify variable names to a, b, c. Without source maps, every stack trace shows the minified names. Debugging is impossible. Sentry's source map upload reverses the minification at display time. The rule makes the upload step non-negotiable and documents the two viable upload methods.
The sampling rule prevents quota exhaustion. Performance monitoring at tracesSampleRate: 1.0 captures every request. On a moderate-traffic site (say 10,000 requests per day), this hits the free-tier quota in under a day. Setting tracesSampleRate: 0.1 captures 10%, which is statistically representative and keeps the quota in budget. The rule sets the production target explicitly.
Install and init pattern
Install the framework-specific SDK. For Next.js:
npx @sentry/wizard@latest -i nextjs
The wizard creates three files at the project root: sentry.client.config.ts, sentry.server.config.ts, sentry.edge.config.ts. Each is the canonical init location for its runtime.
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,
environment: process.env.NEXT_PUBLIC_VERCEL_ENV ?? "development",
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
replaysSessionSampleRate: 0.0,
replaysOnErrorSampleRate: 1.0,
integrations: [
Sentry.replayIntegration({
maskAllText: true,
blockAllMedia: true,
}),
],
beforeSend(event) {
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}
return event;
},
});
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.SENTRY_RELEASE,
environment: process.env.VERCEL_ENV ?? "development",
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
beforeSend(event) {
if (event.user) {
delete event.user.email;
delete event.user.ip_address;
}
if (event.request?.headers) {
delete event.request.headers["authorization"];
delete event.request.headers["cookie"];
}
return event;
},
});
// sentry.edge.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.SENTRY_DSN,
release: process.env.SENTRY_RELEASE,
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0,
});
Three observations:
DSN scoping. The client DSN is
NEXT_PUBLIC_SENTRY_DSN. The server and edge DSN isSENTRY_DSN. They can be the same value but the prefix matters:NEXT_PUBLIC_exposes the variable to the browser bundle. Without it, the client SDK has no DSN and silently drops events.Replay configuration.
replaysSessionSampleRate: 0.0andreplaysOnErrorSampleRate: 1.0is the cost-conscious default. Session replays consume a separate quota tier. Recording 100% of sessions blows through it. Recording only error sessions gives you the context to debug without the cost.PII scrubbing in beforeSend. The
beforeSendhook runs on every event before it ships to Sentry. Deletingevent.user.emailandevent.user.ip_addresskeeps the event scoped touser.idonly. Deleting theauthorizationandcookierequest headers prevents auth tokens from leaking into error reports.
Add the init pattern to CLAUDE.md:
## Init configuration (per runtime)
Three init files, no exceptions:
- sentry.client.config.ts: browser runtime
- sentry.server.config.ts: Node.js runtime (route handlers, server actions)
- sentry.edge.config.ts: Edge runtime (middleware, edge route handlers)
Every init MUST include:
- dsn: from env variable, never hardcoded
- release: from env variable, set by CI to git SHA
- environment: from VERCEL_ENV or NODE_ENV
- tracesSampleRate: 0.1 for production
- beforeSend hook to scrub PII
Claude MUST NOT call Sentry.init() outside these three files.
Source map upload
Source maps map minified production code back to the original TypeScript. Without them, stack traces are unreadable. The Sentry CLI uploads source maps as part of the build.
For Next.js with Vercel, the recommended approach is the build-time plugin:
// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
const nextConfig = {
reactStrictMode: true,
};
module.exports = withSentryConfig(nextConfig, {
org: "your-sentry-org",
project: "your-sentry-project",
authToken: process.env.SENTRY_AUTH_TOKEN,
silent: !process.env.CI,
widenClientFileUpload: true,
hideSourceMaps: true,
disableLogger: true,
});
withSentryConfig wraps the Next.js config and adds the source map upload step to the build. Three settings matter:
widenClientFileUpload: trueuploads source maps for all_next/static/chunksfiles, not just the ones referenced in route manifests.hideSourceMaps: trueremoves the//# sourceMappingURL=comment from the production bundle. The source map is still uploaded to Sentry but not exposed publicly.disableLogger: truestrips Sentry's internal logger calls from the bundle, reducing the JavaScript payload.
The SENTRY_AUTH_TOKEN is a write-scoped token created in Sentry's organisation settings. Add it to Vercel's environment variables as a server-side secret. Claude must never commit it to the repo.
Add a source map section to CLAUDE.md:
## Source maps (production)
- next.config.js wraps with withSentryConfig (Next.js)
- vite.config.ts uses @sentry/vite-plugin (Vite)
- webpack.config.js uses @sentry/webpack-plugin (Webpack)
- SENTRY_AUTH_TOKEN is a CI/build-time secret, NEVER in source
- Verify in Sentry Releases page: artifacts list shows .js.map files
- For server-side bundles, source maps are uploaded automatically by the plugin
- If stack traces still show minified names: check Release Files in Sentry dashboard
Capturing errors correctly
The Sentry SDK provides three primary capture methods. Claude tends to use them interchangeably, which produces noise in the dashboard.
import * as Sentry from "@sentry/nextjs";
// 1. captureException: for caught errors with stack traces
try {
await processOrder(orderId);
} catch (error) {
Sentry.captureException(error, {
tags: { order_id: orderId },
extra: { user_id: userId },
});
throw error; // re-throw if the caller should still handle it
}
// 2. captureMessage: for log-style events without an Error object
Sentry.captureMessage("Payment retry succeeded after 3 attempts", "info");
// 3. captureEvent: for fully structured events (rare, mostly for custom integrations)
Sentry.captureEvent({
message: "Custom event",
level: "warning",
tags: { source: "custom_integration" },
});
The most common Claude mistake is calling console.error(error) instead of Sentry.captureException(error). The console call logs to stderr but does not report to Sentry. Add an ESLint rule or document the difference in CLAUDE.md.
For a Next.js route handler:
// src/app/api/orders/route.ts
import * as Sentry from "@sentry/nextjs";
import { NextRequest, NextResponse } from "next/server";
export async function POST(req: NextRequest) {
try {
const body = await req.json();
const order = await createOrder(body);
return NextResponse.json(order);
} catch (error) {
Sentry.captureException(error, {
tags: { route: "POST /api/orders" },
});
return NextResponse.json(
{ error: "Failed to create order" },
{ status: 500 },
);
}
}
The tags field adds searchable metadata. tags.route lets you filter Sentry's issues view to a specific endpoint. Use it consistently and the dashboard becomes scannable.
Add an error capture section to CLAUDE.md:
## Error capture patterns
- Sentry.captureException(error, { tags, extra }) for caught errors
- Sentry.captureMessage("string", "info|warning|error") for log-style events
- NEVER use console.error(error) in production code, it does not report to Sentry
- Tags: low-cardinality string values (route, env, plan_tier)
- Extra: high-cardinality or large objects (request body, user object, query)
- Re-throw caught errors only if the caller should handle them, otherwise return a typed error
Breadcrumbs and context
Breadcrumbs are the trail of events that preceded an error. Sentry captures most of them automatically (console logs, fetch calls, navigation events). Custom breadcrumbs add domain context.
import * as Sentry from "@sentry/nextjs";
// Add a breadcrumb manually
Sentry.addBreadcrumb({
category: "checkout",
message: "User clicked Pay button",
level: "info",
data: {
cart_total: 49.99,
plan_tier: "pro",
},
});
// Set user context for the rest of the session
Sentry.setUser({
id: user.id,
// NEVER add email or IP without explicit user consent
});
// Set extra context that applies to all subsequent events
Sentry.setContext("plan", {
tier: user.planTier,
trial_active: user.trialActive,
});
// Set tags that apply to all subsequent events
Sentry.setTag("client_version", "2.4.1");
The hierarchy: setUser is the user; setContext is grouped key-value data; setTag is single string values searchable in the UI; addBreadcrumb is timeline events leading up to an error.
For domain-specific scoped errors, withScope:
import * as Sentry from "@sentry/nextjs";
async function processPayment(paymentId: string, amount: number) {
await Sentry.withScope(async (scope) => {
scope.setTag("payment_id", paymentId);
scope.setContext("payment", { amount, currency: "GBP" });
try {
const result = await chargeCard(paymentId, amount);
return result;
} catch (error) {
Sentry.captureException(error);
throw error;
}
});
}
withScope adds the tags and context to any errors captured inside the callback but does not pollute the global scope.
For a deeper look at how Sentry pairs with the Next.js request lifecycle, Claude Code with Next.js covers the middleware and route handler integration points.
Performance monitoring
Performance monitoring captures the full request lifecycle: page loads, route changes, API calls, database queries. The SDK creates transactions automatically; manual spans add granularity for specific operations.
import * as Sentry from "@sentry/nextjs";
async function generateReport(reportId: string) {
return await Sentry.startSpan(
{
name: "generateReport",
op: "task",
attributes: { report_id: reportId },
},
async (span) => {
const data = await Sentry.startSpan(
{ name: "fetchReportData", op: "db.query" },
async () => await db.query("SELECT ..."),
);
const rendered = await Sentry.startSpan(
{ name: "renderReport", op: "task" },
async () => await renderToPdf(data),
);
span.setAttribute("output_size_bytes", rendered.byteLength);
return rendered;
},
);
}
Each startSpan creates a child span attached to the active transaction. The Performance tab in Sentry shows the full waterfall: which operation was slow, how the spans nested, where the time was spent.
Sampling rate matters. tracesSampleRate: 0.1 captures 10% of transactions. For low-traffic apps that is too few. For high-traffic apps it is correct. The right number depends on volume.
| Daily request volume | tracesSampleRate | Captured transactions/day |
|---|---|---|
| Under 1,000 | 1.0 | All |
| 1,000 to 10,000 | 0.5 to 1.0 | Half to all |
| 10,000 to 100,000 | 0.1 to 0.3 | 1k to 30k |
| Over 100,000 | 0.05 to 0.1 | 5k to 10k |
For more nuanced control, tracesSampler accepts a function that returns a per-transaction rate:
Sentry.init({
// ...
tracesSampler: (samplingContext) => {
if (samplingContext.request?.url?.includes("/healthcheck")) {
return 0;
}
if (samplingContext.request?.url?.includes("/checkout")) {
return 1.0;
}
return 0.1;
},
});
This pattern down-weights low-value transactions (health checks, asset requests) and up-weights critical paths (checkout, signup).
Add performance monitoring to CLAUDE.md:
## Performance monitoring
- tracesSampleRate: 0.1 production default, adjust by volume
- For critical paths: tracesSampler function returns 1.0
- For noise (healthcheck, static asset): return 0
- Manual spans: Sentry.startSpan for sub-transaction granularity
- Span op values: "task", "db.query", "http.client", "function" (use consistently)
- Attributes on spans: low-cardinality, searchable in the UI
Looking to ship error tracking that produces actionable reports? Get Claudify. Pre-built CLAUDE.md templates for Sentry and every major observability tool, ready to drop into your project.
Release tracking
The release identifier ties errors to the deploy that introduced them. Without it, "did the fix work" requires manually correlating timestamps with deploys. With it, Sentry shows release-over-release issue counts and "regressed in this release" filters.
In CI (GitHub Actions, Vercel, etc.), set the release env var before the build:
# .github/workflows/deploy.yml
- name: Set release
run: echo "SENTRY_RELEASE=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
- name: Build
run: npm run build
env:
NEXT_PUBLIC_SENTRY_RELEASE: ${{ env.SENTRY_RELEASE }}
SENTRY_RELEASE: ${{ env.SENTRY_RELEASE }}
Sentry CLI creates the release record before the deploy and finalises it after:
# Before deploy
sentry-cli releases new "$SENTRY_RELEASE"
sentry-cli releases set-commits "$SENTRY_RELEASE" --auto
sentry-cli releases files "$SENTRY_RELEASE" upload-sourcemaps ./.next/static
# After deploy
sentry-cli releases finalize "$SENTRY_RELEASE"
sentry-cli releases deploys "$SENTRY_RELEASE" new --env production
set-commits --auto uses git history to associate commits with the release. deploys new creates a deploy marker, which lets Sentry annotate event timelines with deploy lines.
Add release tracking to CLAUDE.md:
## Releases (CI)
- SENTRY_RELEASE = short git SHA (or semver tag for tagged releases)
- Init reads release: process.env.SENTRY_RELEASE
- sentry-cli releases new $SENTRY_RELEASE before build
- sentry-cli releases set-commits $SENTRY_RELEASE --auto after build
- sentry-cli releases finalize $SENTRY_RELEASE after deploy
- sentry-cli releases deploys $SENTRY_RELEASE new --env production
- This pipeline is automated in CI, NEVER run from a developer machine
Common Claude Code mistakes with Sentry
Six patterns Claude generates incorrectly without CLAUDE.md constraints, with the correct replacement for each.
1. Multiple init calls
Claude generates: Sentry.init({ ... }) at the top of src/lib/sentry.ts and again in src/middleware.ts.
Correct pattern: one init per runtime in the framework-required init file, no others.
2. Hardcoded DSN
Claude generates: dsn: "https://abc@o123.ingest.sentry.io/456" inline.
Correct pattern: dsn: process.env.NEXT_PUBLIC_SENTRY_DSN.
3. tracesSampleRate: 1.0 in production
Claude generates: tracesSampleRate: 1.0 everywhere.
Correct pattern: tracesSampleRate: process.env.NODE_ENV === "production" ? 0.1 : 1.0.
4. console.error instead of captureException
Claude generates: } catch (error) { console.error(error); }.
Correct pattern: } catch (error) { Sentry.captureException(error); throw error; }.
5. Missing source map upload
Claude generates: a Next.js config that imports Sentry without withSentryConfig.
Correct pattern: module.exports = withSentryConfig(nextConfig, { authToken, ... }).
6. PII in event payloads
Claude generates: Sentry.setUser({ id, email, ip }).
Correct pattern: Sentry.setUser({ id }), scrub email and IP in beforeSend.
| Mistake | Symptom | Fix |
|---|---|---|
| Multiple init | Scope conflicts, missed events | One init per runtime |
| Hardcoded DSN | Secret in repo, can't rotate | Env var |
| tracesSampleRate 1.0 | Quota burn | 0.1 in production |
| console.error | Errors never reach Sentry | captureException |
| No source maps | Unreadable stack traces | withSentryConfig |
| PII in events | GDPR / CCPA violation | beforeSend scrub |
Add a common mistakes section to CLAUDE.md with these six pairs. Concrete pairs are more reliable than abstract rules for Claude to apply consistently.
Permission hooks for Sentry tooling
A Sentry project accumulates scripts: release creation, source map upload, issue triage, alert rule sync. Most are read-only or controlled by CI, but some can destructively modify dashboard config. Permission hooks gate the destructive ones.
In .claude/settings.local.json:
{
"permissions": {
"allow": [
"Bash(npx sentry-cli releases list*)",
"Bash(npx sentry-cli info*)",
"Bash(npx sentry-cli sourcemaps inject*)"
],
"deny": [
"Bash(npx sentry-cli releases delete*)",
"Bash(node scripts/sync-alert-rules.js*)",
"Bash(node scripts/bulk-resolve-issues.js*)"
]
}
}
Listing releases and inspecting source map state is safe. Deleting releases or bulk-resolving issues needs explicit confirmation. The deny list forces Claude to surface those operations as prompts rather than running them mid-task.
Building error tracking that produces actionable reports
The Sentry CLAUDE.md in this guide produces error tracking code where each runtime has exactly one init, the DSN is read from environment variables, source maps are uploaded as part of every production build, releases tie errors to deploys, sampling stays within quota, PII is scrubbed before events leave the process, and captureException replaces console.error everywhere.
The underlying principle is the same as any observability integration with Claude Code. Sentry without a CLAUDE.md produces code that looks correct and successfully reports events, but produces dashboards that are unactionable: stack traces with minified variable names, errors without release attribution, performance data that exhausts the quota, and event payloads that violate GDPR. The CLAUDE.md template removes each failure mode by making the correct pattern the only pattern Claude can generate.
For the next layer up from error tracking, Sentry pairs well with Claude Code with PostHog for product analytics that complement error reports, and Claudify includes a Sentry CLAUDE.md template with the three-init pattern, source map pipeline, release tagging, sampling targets, PII scrubbing, and all six common-mistake pairs pre-configured.
Get Claudify. Ship production-ready Sentry integrations with Claude Code from the first session.
More like this
Ready to upgrade your Claude Code setup?
Get Claudify