Production Error Tracking: Catch Bugs Before Your Users Report Them
Your app works perfectly on localhost but crashes in production. Set up Sentry and error tracking to catch bugs before users complain.
Your app works perfectly on localhost but crashes in production. Set up Sentry and error tracking to catch bugs before users complain.
How to deploy without shutting down servers. Differences between Rolling, Canary, and Blue-Green. Deep dive into Database Rollback strategies, Online Schema Changes, AWS CodeDeploy integration, and Feature Toggles.

Solving server waste at dawn and crashes at lunch. Understanding Auto Scaling vs Serverless through 'Taxi Dispatch' and 'Pizza Delivery' analogies. Plus, cost-saving tips using Spot Instances.

Why your server isn't hacked. From 'Packet Filtering' checking ports/IPs to AWS Security Groups. Evolution of Firewalls.

Why would Netflix intentionally shut down its own production servers? Explore the philosophy of Chaos Engineering, the Simian Army, and detailed strategies like GameDays and Automating Chaos to build resilient distributed systems.

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.
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.
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;
},
});
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.
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.
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."
Can't watch the Sentry dashboard 24/7. Critical errors go to Slack:
Alert Rule Examples:payment or auth# .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.
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.
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.
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.
Sentry free tier: 5k events/month. Was enough initially, but hit the limit in 3 days once traffic grew.
Solutions:
ResizeObserver.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.
What changed after setting up error tracking:
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.