Quick start for Next.js
The MIT licensed @sentry/nextjs package, unchanged, with its DSN pointed here. Four files, one shared set of options, and an issue list that still tells the browser, the server and the edge apart.
Before you start
A project in Monitor, and its DSN.
Create the project, then open its Monitor settings screen and copy the DSN from the key card. Set it as an environment variable on your host rather than pasting it into the repository, not because it is secret but because it differs per project; the config below reads UNREALOPS_DSN.
One DSN for the project, not one per environment. The environment is a label the SDK puts on the event, and it is set at build time below, so a preview deployment and production share one address and stay separable.
Point the app at it
Add the package
npm install @sentry/nextjsDo not run the Sentry wizard. It exists to configure uploads to Sentry's own backend and it will add a build plugin you do not need here. If your build already uses the Sentry bundler plugin for other reasons, leave it: Monitor reads the debug ids it injects and prefers them to matching by path.
Write the options once
Three configs are about to read the same values. Putting them in one module is what stops the browser bundle and the server from disagreeing about which release they are, which is the disagreement that makes source maps resolve against the wrong build.
/**
* One place the three configs below read, so the DSN, the environment and the
* release cannot drift between the browser bundle and the server.
*/
export function monitorOptions() {
return {
dsn: process.env.NEXT_PUBLIC_UNREALOPS_DSN,
// Nothing sets this for you. Without it a preview deployment's crashes
// are indistinguishable from production's.
environment: process.env.NEXT_PUBLIC_UNREALOPS_ENVIRONMENT ?? "development",
// The release your events name has to be the release your source maps
// were uploaded under, so both sides read the same commit sha.
release: process.env.NEXT_PUBLIC_UNREALOPS_RELEASE || undefined,
// Zero to start with, so the first thing you see is a crash rather than
// an allowance already spent. Transactions are stored, and each one costs
// a whole event out of the same monthly pool your errors come from, so
// raise this deliberately and raise it a little.
tracesSampleRate: 0,
};
}import type { NextConfig } from "next";
const nextConfig: NextConfig = {
// Inlined at build time, because a browser bundle cannot read process.env
// and the browser half needs all three of these. The DSN's public key is
// public by construction: it ships in the bundle either way and can only
// write.
env: {
NEXT_PUBLIC_UNREALOPS_DSN: process.env.UNREALOPS_DSN ?? "",
NEXT_PUBLIC_UNREALOPS_ENVIRONMENT: process.env.VERCEL_ENV ?? "development",
NEXT_PUBLIC_UNREALOPS_RELEASE: process.env.VERCEL_GIT_COMMIT_SHA ?? "",
},
};
export default nextConfig;Initialize all three runtimes
Three initializations reading one set of options, so the same DSN reaches all three. That is what makes browser, server and edge errors land in one project while staying tellable apart on the issue.
import * as Sentry from "@sentry/nextjs";
import { monitorOptions } from "./monitor";
Sentry.init(monitorOptions());
// Router navigations, so a client error carries the route it happened on.
export const onRouterTransitionStart = Sentry.captureRouterTransitionStart;import * as Sentry from "@sentry/nextjs";
import { monitorOptions } from "./monitor";
Sentry.init(monitorOptions());import * as Sentry from "@sentry/nextjs";
import { monitorOptions } from "./monitor";
Sentry.init(monitorOptions());import * as Sentry from "@sentry/nextjs";
export async function register(): Promise<void> {
if (process.env.NEXT_RUNTIME === "nodejs") {
await import("./sentry.server.config");
}
if (process.env.NEXT_RUNTIME === "edge") {
await import("./sentry.edge.config");
}
}
// Next's own hook, called for server components, route handlers and
// middleware alike. Without this line, server side errors are never reported:
// a healthy request never touches it, and an unhealthy one has already failed
// by the time it does.
export const onRequestError = Sentry.captureRequestError;All four sit at the root of your project, or under src/ if that is where your app lives. The client file is loaded by Next before any application code, so an error thrown during the first render is still caught.
Throw once on each side
Two deliberate crashes rather than one, because they prove different halves of the wiring. If only the browser one arrives, the missing piece is almost always instrumentation.ts.
// app/api/monitor-check/route.ts
export function GET() {
throw new Error("first event from the Next.js server");
}"use client";
export function BreakOnPurpose() {
return (
<button
type="button"
onClick={() => {
throw new Error("first event from the browser");
}}
>
Break on purpose
</button>
);
}Delete both once you have seen them. They are proof, not instrumentation.
Each event says which runtime it came from
Because the same message means two different things on the two sides.
Nothing on the wire distinguishes them by itself. The SDK reports platform: javascript from the browser and from the edge alike, and names itself nextjs in all three places. What separates them is the runtime context the server side attaches, and Monitor reads that rather than guessing: every event carries an origin of browser, server or edge, and the issue list shows it.
It matters more than it sounds. TypeError: x is not a function in a browser is a bad bundle reaching a user, and the same string on the server is a request that returned a 500. An issue list that rendered them identically would make you work that out from the stack every time.
Set an environment on the first day
The SDK sets none by default, and Vercel gives you the right value for free.
VERCEL_ENV is production, preview or development, which is exactly the distinction you want on an issue. Without it, every preview deployment, every branch and production all report the same environment, which is none, and the environment filter has nothing to filter on.
If you are not on Vercel, set NEXT_PUBLIC_UNREALOPS_ENVIRONMENT yourself in the build. The value is a label on the event rather than a property of the DSN, which is what lets one address serve every environment you run.
Releases need no configuration here either
There is nothing to create in Monitor and no finalize step.
Monitor creates a release the first time an event names one. On the server the SDK fills that in by itself from the build's commit sha, Vercel's included, so server events already carry a release with nothing configured.
The browser is the half that does not: a bundle cannot read process.env, so unless the value is inlined at build time, browser events carry no release while server events do. That is what the NEXT_PUBLIC_UNREALOPS_RELEASE line in next.config.ts above is for, and it becomes load bearing the moment you upload source maps: a frame is matched to a map by release, and two halves naming different releases resolve against different builds.
What to do next
- Readable stack traces. A production build is minified on both sides, so until you upload source maps every frame is a one letter function in a content hashed chunk. This is the page that matters most for Next.js.
- Moving from Sentry. One field points a copy of everything at your old DSN, so you can compare both backends on your own crashes.
- Add an address under Alerts on the Monitor settings screen. Nothing is emailed until you do.
- If only half of it arrived, the troubleshooting page has that symptom by name.