Production Error Tracking: Catch Bugs Before Your Users Report Them
Prologue: "Works on My Machine"
I shipped the feature and took a breath. Tested locally. Checked staging. Everything worked. Then I got a Discord message.
"The signup button doesn't do anything."
I rushed to check. My browser? Works fine. Console? No errors. I asked the user for a screenshot. Just a blank white screen. As I typed "Can you try again?", I realized: I had no idea what was happening in my own app.
Production is not localhost. Your users' browsers are not your MacBook. Works in Chrome, breaks in Samsung Internet. Works in NYC, times out in Vietnam. User A is fine, User B crashes every time.
console.log only prints to your terminal. User errors are silently swallowed. Like screaming in space—nobody can hear you.
I finally got it. In production, you need to see what you can't see. You need to know about errors before users report them. So I added Sentry.
Aha Moment: Errors Flying In Real-Time
First day after setting up Sentry, my Slack pinged:
[Sentry] New Issue: Cannot read property 'map' of undefined
in ProfilePage.tsx:45
User: user-abc-123
Browser: Safari 15.2
5 occurrences in the last hour
Oh. That bug. The profile page renders a follower list, but I never handled the case where the API returns null. Worked locally because my account had followers, but new users got null, not even an empty array.
Fixed in 5 minutes. Before users complained.
That's when it clicked. Error tracking isn't just logging—it's protecting user experience proactively. Like a smoke detector catching fires before someone calls 911.
Deep Dive: From Setup to Production-Ready
1. Adding Sentry to Next.js
The easiest way is the official wizard:
npx @sentry/wizard@latest -i nextjs
But I wanted to know what I was installing. So I did it manually.
// sentry.client.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
// 100% sampling in production, off in dev
tracesSampleRate: process.env.NODE_ENV === "production" ? 1.0 : 0,
// Track deploy versions - this is crucial
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
// Separate environments
environment: process.env.NEXT_PUBLIC_VERCEL_ENV || "development",
// Filter sensitive data
beforeSend(event, hint) {
// Don't send from localhost
if (window.location.hostname === "localhost") {
return null;
}
// Remove password fields
if (event.request?.data) {
delete event.request.data.password;
delete event.request.data.token;
}
return event;
},
// Ignore known noise
ignoreErrors: [
"ResizeObserver loop limit exceeded", // browser bug
"Non-Error promise rejection captured", // library warnings
],
});
Server-side needs separate config:
// sentry.server.config.ts
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
tracesSampleRate: 1.0,
// Server errors need more context
integrations: [
new Sentry.Integrations.Http({ tracing: true }),
],
beforeSend(event) {
// Don't leak environment variables
if (event.contexts?.runtime?.environment) {
delete event.contexts.runtime.environment;
}
return event;
},
});
2. Source Maps: Making Minified Code Readable
Production builds are minified. Error stacks look like t.map is not a function at r (chunk-abc.js:1:2345). To read them, upload source maps.
// next.config.js
const { withSentryConfig } = require("@sentry/nextjs");
module.exports = withSentryConfig(
{
// Your Next.js config
},
{
// Sentry webpack plugin config
silent: true, // quiet build logs
org: "your-org",
project: "your-project",
// Upload source maps
widenClientFileUpload: true,
// Tree-shake Sentry in production
hideSourceMaps: true,
// Auto-remove debug info
disableLogger: true,
}
);
Now errors show the original TypeScript file and exact line number.
3. Error Fingerprinting: Group Same Errors
Initially, the same error created separate issues for each user. Sentry grouped by URL or user ID. Fixed it with fingerprinting:
Sentry.init({
beforeSend(event, hint) {
// Group by error type and location only
if (event.exception?.values?.[0]) {
const error = event.exception.values[0];
event.fingerprint = [
error.type || "Error",
error.value || "Unknown",
error.stacktrace?.frames?.[0]?.filename || "unknown",
];
}
return event;
},
});
Now 100 users hitting the same bug = 1 issue. Less noise.
4. Custom Context: Info Needed to Reproduce
Error messages alone aren't enough. Send user state too:
import * as Sentry from "@sentry/nextjs";
// Set user info (on login)
Sentry.setUser({
id: user.id,
email: user.email,
username: user.username,
});
// Extra context
Sentry.setContext("subscription", {
plan: user.plan,
status: user.subscriptionStatus,
expiresAt: user.subscriptionExpiresAt,
});
// Leave breadcrumbs for specific actions
Sentry.addBreadcrumb({
category: "payment",
message: "User clicked checkout button",
level: "info",
data: {
amount: 99,
currency: "USD",
},
});
// Manually capture errors with context
try {
await processPayment();
} catch (error) {
Sentry.captureException(error, {
tags: {
payment_method: "stripe",
flow: "checkout",
},
extra: {
cartItems: cart.items.length,
totalAmount: cart.total,
},
});
}
Now errors show context like "free plan user clicked checkout."
5. Alerts: Get Notified in Slack
Can't watch the Sentry dashboard 24/7. Critical errors go to Slack:
Alert Rule Examples:
- New error type appears
- Same error happens 10+ times in an hour
- Error rate exceeds 5%
- Errors tagged
paymentorauth
# .sentry/alerts.yaml (declarative config)
- name: "Critical Payment Errors"
conditions:
- type: "event.type"
match: "error"
- type: "event.tag"
key: "transaction"
match: "contains"
value: "/api/payment"
actions:
- type: "slack"
workspace: "your-workspace"
channel: "#alerts-critical"
Got woken up at 3am once. Payment API was down. Fixed it before users complained.
6. Release Tracking: Which Deploy Broke It?
Use Git SHA as release version to correlate errors with deploys:
# Create release on deploy
sentry-cli releases new $VERCEL_GIT_COMMIT_SHA
sentry-cli releases set-commits $VERCEL_GIT_COMMIT_SHA --auto
sentry-cli releases finalize $VERCEL_GIT_COMMIT_SHA
Vercel sets this automatically:
// sentry.client.config.ts
Sentry.init({
release: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});
Now Sentry says "this error only appeared after commit abc1234." Immediately know which PR broke it.
7. Performance Monitoring: Find Slow Pages
Sentry tracks performance too, not just errors:
Sentry.init({
tracesSampleRate: 0.1, // only 10% sampling (save costs)
integrations: [
new Sentry.BrowserTracing({
// Only track specific URLs
tracePropagationTargets: [
"localhost",
/^https:\/\/yourapp\.com\/api/,
],
}),
],
});
See API call times, page loads, slow components. Discovered the profile page took 5 seconds on average, optimized data fetching.
Sentry vs Alternatives: Which One?
Too many choices initially. Here's what I learned:
Sentry: Error tracking specialist. Open-source, self-hostable. Generous free tier (5k events/month). Startup-friendly. I chose it because it's quick to start and customizable later.
LogRocket: Powerful session replay. See exactly what users clicked and typed, like a video recording. But expensive. Overkill for early-stage startups.
Datadog: Full infrastructure monitoring. Logs, metrics, APM, errors—all in one. But enterprise pricing. Not at that stage yet.
Rollbar: Sentry competitor. Similar features but less intuitive UI. Smaller community.
Ended up with Sentry + Vercel Analytics + PostHog (user analytics). Each tool handles its specialty, no overlap.
Cost Management: Startup-Friendly Setup
Sentry free tier: 5k events/month. Was enough initially, but hit the limit in 3 days once traffic grew.
Solutions:
- Sampling: Don't send every error. 10% sampling still shows patterns.
- Filtering: Ignore browser noise like
ResizeObserver. - Disable Performance: Use error tracking only, add performance later.
- Separate Environments: Don't send staging errors to Sentry.
Sentry.init({
// Capture 100% of errors, but sample performance
tracesSampleRate: process.env.NODE_ENV === "production" ? 0.05 : 0,
beforeSend(event) {
// Custom rate limiting
if (Math.random() > 0.3) { // only send 30%
return null;
}
return event;
},
});
Stayed on free tier this way.
The Bottom Line
What changed after setting up error tracking:
- Know before users report: Fixed bugs before they hit Twitter.
- Catch unreproducible bugs: "Doesn't work for me" is no longer a mystery. See browser, OS, user state.
- Ship fearlessly: Used to avoid Friday deploys. Now deploy anytime because I'll know immediately if something breaks.
- Data-driven priorities: "This bug affected 100 users" makes priorities crystal clear.
It's like installing a dashcam in your car. When something crashes, you know exactly what happened.
Flying blind in production is terrible. Now I drive with my eyes open. Thanks to Sentry.