Sentry, or a production error with its context
A server log says something crashed. Sentry says what crashed, for how many users, since which deployment, on which line, and what the user was doing moments earlier.
That difference decides whether a fix takes ten minutes or half a day. Instead of reconstructing a problem from a customer's description, you get a stack trace, variable values, the browser, the application version, and the sequence of events leading to the failure.
The tool today covers three layers: errors, tracing of slow operations, and session replay. All three are billed separately, and that is the first thing to understand, since the bill follows from it.
Billing units
Plans describe not the number of people but the number of events of each kind, and each kind grows differently.
An error is a single event sent after a failure. Every occurrence counts rather than every kind of error, so one faulty loop across a thousand users is a thousand events even though the problem is one.
A tracing span describes one operation within a request: a database query, an API call, a component render. One request generates dozens of them, so that line grows far faster than errors and under moderate traffic is often the largest in the whole bill.
A session recording captures what a user saw. It costs the most per unit and delivers the most information on errors that cannot be reproduced.
Logs and application metrics, billed by volume, add to that.
The free plan covers one person and an error allowance sufficient for a side project. Team plans include tens of thousands of errors, several million tracing spans, and a replay allowance, with usage billed above the threshold. Specific rates change over time, so check the current price list before budgeting.
Note that reserving capacity in advance is cheaper than paying for actual usage. Under steady traffic the difference reaches a dozen or so percent, and reservations can be adjusted monthly.
How not to overpay
The bill for this tool grows in ways that surprise people, so three mechanisms deserve enabling from day one.
The first is trace sampling. Sending every request rarely makes sense, since diagnosing a slow page needs a few percent of traffic. Setting sampling to a dozen or so percent cuts that line many times over while preserving the picture of what is slow.
Sentry.init({
dsn: process.env.SENTRY_DSN,
tracesSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0.01,
})Note the two replay settings. Recording every session is expensive and unnecessary, since you will watch a dozen or so. Recording one percent of ordinary sessions and all sessions ending in an error gives the same insight at a fraction of the price.
The second mechanism is filtering noise before sending. Browser extensions, bots, and user side network errors generate events you will not fix anyway and which count towards the limit.
beforeSend(event, hint) {
const error = hint.originalException
if (error instanceof Error && error.message.includes('ResizeObserver')) {
return null
}
return event
}The third is spike protection and a spending limit. A deployment with an error inside a loop can generate a monthly allowance within an hour, and a mechanism detecting a sudden rise halts ingestion before that happens.
A fourth thing, simpler than all the above: review the list of most frequent events once a month. It usually turns out a third of the allowance goes to two or three problems that can be fixed or filtered in a quarter of an hour.
Source maps and releases
This is the part whose omission renders the whole tool pointless, and it happens regularly nonetheless.
Code shipped to the browser is minified, so a stack trace without source maps points at line three of a randomly named file. Source maps translate that back into your code, with function names and line numbers.
Maps must be uploaded during the build rather than exposed publicly. Public maps mean anybody can reconstruct your source code, so build tool plugins send them straight to the service and remove them from the bundle.
import { withSentryConfig } from '@sentry/nextjs'
export default withSentryConfig(config, {
org: 'my-company',
project: 'shop',
authToken: process.env.SENTRY_AUTH_TOKEN,
sourcemaps: { deleteSourcemapsAfterUpload: true },
silent: !process.env.CI
})The last option in the sourcemaps block matters most here: the maps reach the service and then disappear from the output directory, so they never travel to the server with the application.
In a monorepo with build caching there is one more thing to check, because it breaks quietly. Turborepo replays a task's result from cache instead of running the command when the inputs have not changed, and the map upload is a side effect of that command, so on a cache hit it simply does not happen. The release ships without maps, and the cause is invisible in the log, because the build succeeded. There are two fixes: add the release identifier to the task's inputs so every release computes afresh, or move the upload into a separate step outside the cached task.
The second thing is tagging releases. Without it you know an error occurs; with it you know it appeared with Tuesday's deployment and did not occur before. That usually shortens the hunt for a cause from hours to minutes, since instead of reading a whole module you read one changeset.
Linking releases to repository changes gives one more thing: pointing at the likely author of the change that introduced the problem. That is not a tool for holding people to account but for quickly finding the person who knows what was happening in that code.
export SENTRY_RELEASE=$(git rev-parse --short HEAD)
npx sentry-cli releases new "$SENTRY_RELEASE"
npx sentry-cli releases set-commits "$SENTRY_RELEASE" --auto
npx sentry-cli releases finalize "$SENTRY_RELEASE"The short commit hash as a release identifier suffices and has the advantage of requiring nobody's decision. All that matters is that the same value reaches the application configuration, otherwise reports attach to a release nobody deployed.
Client and server setup
Configuration differs between layers, and applications rendered on the server require handling both.
On the browser side the tool captures unhandled exceptions, rejected promises, and resource errors. Set right away which addresses should be traced on network requests.
Sentry.init({
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
release: process.env.NEXT_PUBLIC_SENTRY_RELEASE,
environment: process.env.NODE_ENV,
tracesSampleRate: 0.1,
replaysOnErrorSampleRate: 1.0,
replaysSessionSampleRate: 0,
tracePropagationTargets: [/^https:\/\/api\.mycompany\.com/]
})The last option solves a problem that under default settings shows up as suddenly rejected requests: tracing headers travel everywhere, including to third party services, which reject them under cross origin rules. The two options above concern session replay and are set so a recording is produced only on an error rather than on every visit.
On the server side exceptions from request handling are captured, and with Next.js an edge layer joins in, requiring its own configuration file. Three files instead of one is one of those things that look like a complication and follow from the code running in three different environments.
Set the user context after login, since without it you know how many occurrences there were rather than how many people they affected. An identifier suffices, without an email address or personal data, if policy requires that.
Sentry.setUser({ id: user.id })
Sentry.setTag('plan', user.plan)Choose tags carefully. A few values with a small number of variants, a plan or a region for instance, let you filter the issue list sensibly. A tag taking thousands of distinct values gives nothing beyond cluttering the interface.
When deploying on platforms like Vercel, attach a deployment completion notification so releases in the tool match real ones without manual work.
Sentry against the alternatives
| Option | Strength | Weakness | Pick it when |
|---|---|---|---|
| Sentry | Deep error diagnostics, execution context | Bill grows with traffic, many units | A team fixing production errors |
| PostHog | Product analytics, recordings, experiments | Shallower error diagnostics | Emphasis on user behaviour |
| Cloud provider logs | Everything in one place, cheap to start | No grouping or error context | A backend without a user interface |
| Self hosting | Data stays put, no per event fees | Cluster upkeep is yours | Data residency requirements |
The first row wins where time from report to fix matters. Grouping identical errors into one issue, the count of affected users, and execution context are things ordinary logs do not give and which determine what to fix first.
The last row deserves honest consideration. The project is open and can be run yourself, while handling a large event volume requires considerable infrastructure, so under serious traffic it can cost more than the service. That choice is usually driven by legal requirements rather than savings.
What to do with what you see
A tool showing five hundred issues is as useless as no tool, so a way of working with that list pays off.
Sort by affected users rather than by occurrence count. An error occurring ten thousand times for one user is a smaller problem than one occurring a hundred times across a hundred people, and the first sort puts it at the top.
Decide what is an error and what is noise. An aborted request when a tab closes, an exception from a browser extension, and a network error on a mobile user's side are events you will not fix. Filter them out rather than scrolling past them every time.
Assign issues to people and close them deliberately. A list where nobody closes anything stops being read within a quarter, and then the tool stops giving anything.
Set notifications narrowly. A message on every new kind of error is acceptable; a message on every occurrence is not. A channel receiving four hundred notifications a day gets muted within a week, and then nobody notices the one that mattered.
Use marking issues as resolved in a given release too. An error returning three months later is then reported as a regression rather than a new problem, and you know immediately the earlier fix missed something.
Sensitive data in reports
This part gets skipped and concerns personal data leaving your infrastructure without anybody deciding it should.
An error report carries context, and the context is often richer than assumed. Request bodies, headers, variable values at the point of failure, and the page address with its parameters. A registration form that crashed can transmit the password a user typed, because it was a variable in scope.
The tool removes some such values automatically, recognising common field names, and that does not suffice. A field name specific to your application will not be recognised, so extend the removal list yourself.
Session recordings need separate attention, since they capture what a user sees. Masking form fields is the default and deserves keeping, with explicit exclusions of whole page regions added for particularly sensitive fields.
Page addresses carry data too, if identifiers in the path can be linked to a person. That is rarely a problem in itself and becomes one alongside the rest of the context.
Practical advice: before enabling this in production, deliberately trigger an error on a page holding a form and read exactly what arrived in the interface. Ten minutes of that work answers a question the documentation answers only in general terms.
Common mistakes
The first is sending every request to tracing. A dozen or so percent of traffic suffices for diagnosis, and the difference in the bill is manifold.
The second is recording every session. Recordings of sessions ending in an error plus a small percentage of the rest give the same insight far more cheaply.
The third is no source maps. A stack trace pointing at minified code says nothing, and uploading maps during the build is one plugin.
The fourth is skipping release tagging. Without it you do not know which deployment introduced a problem, and that is usually the fastest route to the cause.
The fifth is notifications on every occurrence. A channel flooded with notifications gets muted, and then you miss the one that mattered.
The sixth is no spike protection. A deployment with an error inside a loop can exhaust a monthly allowance within an hour.
The seventh is sending reports without checking what is actually in them. Crash context is often richer than assumed and can carry data that should not leave your infrastructure.
FAQ
What does Sentry cost?
The free plan covers one person and an error allowance sufficient for a side project. Team plans include set allowances of errors, tracing spans, and recordings, with usage billed above the threshold. Reserving in advance is cheaper than paying as you go, and rates deserve checking on the current price list.
Why does the bill grow faster than traffic?
Because one request generates dozens of tracing spans rather than one event. That line grows fastest and under moderate traffic is often the largest, so sampling at around a dozen percent is the most important setting here.
What are source maps for?
Code shipped to the browser is minified, so without maps a stack trace points at an unreadable file. Maps translate it back into your code with function names and line numbers. Upload them during the build and do not expose them publicly.
Will it replace product analytics?
No. Sentry answers what is breaking and for whom, while tools like PostHog answer what users do and where they give up. A typical team uses both, each for something different.
Can it be self hosted?
Yes, the project is open. Handling a large event volume requires considerable infrastructure, though, so under serious traffic self hosting can cost more than the service. That choice is usually driven by data residency requirements.
Documentation sits on the project site, and quota management in a separate section.