Every deploy is a guessing game without versioning: an error lands in your dashboard at 14:23 UTC, you check the deployment logs, and find three versions deployed in the last hour. Which one broke? Without tagging errors with next_public_app_version in your Next.js app, you're debugging blind. With it, every exception carries the exact build that caused it—so you can answer "did my deploy cause this?", "is this fixed in prod yet?", and most critically, "which build do these source maps belong to?" in seconds instead of hours.
This guide covers how to wire next_public_app_version into your Next.js app, tag every error with it, and connect that release metadata to your error tracking so you can correlate regressions to exact deploys.
Why NEXT_PUBLIC_ matters
Next.js inlines NEXT_PUBLIC_* environment variables directly into your client bundle at build time, not at runtime. This is the exact mechanic that makes release tracking work—and the thing that trips up most teams.
When you set NEXT_PUBLIC_APP_VERSION=v1.2.3 at build time and run npm run build, Next.js uses a DefinePlugin to replace every reference to process.env.NEXT_PUBLIC_APP_VERSION with the literal string "v1.2.3" in the compiled JavaScript. That value is now baked into your artifact permanently. If you deploy that same build to staging and production, both get the same version string. If you try to set it at deploy time (e.g., via a Docker runtime environment variable), it has zero effect on client code—you must rebuild to change it.
This is the crucial difference: NEXT_PUBLIC_* is a build artifact property, not a runtime config. The consequence is powerful: every single JavaScript bundle ever built carries its own version identifier, so a minified stack trace from production can be de-minified with exactly the right source map by matching both the minified code AND the release tag.
A common mistake is setting NEXT_PUBLIC_APP_VERSION as a secret in your deployment platform and expecting it to work without rebuilding. It won't. The value must be injected at build time, not at runtime.
Choosing a version value
You have three main options: a git SHA, a semantic version from package.json, or a CI build number. Each has trade-offs.
Git SHA (recommended): Run git rev-parse --short HEAD in your build to get a unique, unambiguous identifier for every commit. It never changes, requires no manual versioning, and links directly to your source:
export NEXT_PUBLIC_APP_VERSION=$(git rev-parse --short HEAD)
npm run build
On Vercel (or GitHub Actions, GitLab CI, etc.), this is even easier. Vercel automatically provides the git SHA as VERCEL_GIT_COMMIT_SHA. You can expose it to the client by prefixing it as NEXT_PUBLIC_:
# In your Vercel project settings, or in your build command:
export NEXT_PUBLIC_APP_VERSION=$VERCEL_GIT_COMMIT_SHA
npm run build
Semantic version: If you use npm version patch to bump package.json, you can read that:
// next.config.ts
import { readFileSync } from "fs";
const packageJson = JSON.parse(readFileSync("./package.json", "utf8"));
const config = {
env: {
NEXT_PUBLIC_APP_VERSION: packageJson.version,
},
};
export default config;
This is human-readable but requires discipline—developers must remember to bump the version on every deploy.
CI build number: Some teams use a monotonic build ID (e.g., GitHub Actions' $GITHUB_RUN_ID or a custom counter):
export NEXT_PUBLIC_APP_VERSION="build-$GITHUB_RUN_ID"
npm run build
Unique and automatic, but less intuitive when correlating with commit history.
Pick one scheme and stick with it across all services (web app, API, worker functions). Consistency matters more than perfection. Many teams use git SHA on dev/staging and semantic version in production for human readability.
Wiring it into your Next.js app
Once you have a version string at build time, pass it to the Sentry SDK during initialization. The SDK needs it so every error event carries the release identifier.
Option 1: Via next.config.ts
// next.config.ts
import { NextConfig } from "next";
const config: NextConfig = {
env: {
NEXT_PUBLIC_APP_VERSION: process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0-dev",
},
};
export default config;
Then in your app initialization:
// app/layout.tsx or pages/_app.tsx
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
release: `web@${process.env.NEXT_PUBLIC_APP_VERSION}`,
environment: process.env.NODE_ENV,
tracesSampleRate: 1.0,
});
Option 2: Direct environment variable
// app/layout.tsx
import * as Sentry from "@sentry/nextjs";
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
release: `web@${process.env.NEXT_PUBLIC_APP_VERSION || "0.0.0-dev"}`,
environment: process.env.NODE_ENV,
});
For server-side code (API routes, server components), you can use process.env.NODE_VERSION if you're setting it as a regular (non-public) environment variable, or import it the same way. The critical part is that both client and server report the same release string.
// app/api/health/route.ts
import * as Sentry from "@sentry/nextjs";
export async function GET() {
Sentry.captureMessage("Health check", {
release: `api@${process.env.NEXT_PUBLIC_APP_VERSION}`,
});
return Response.json({ status: "ok" });
}
Source maps must match the release
The whole point of tagging releases is tying them to source maps. A minified stack trace like bundle-abc.js:1:45923 is meaningless without the corresponding source map that says "that's line 42 in your Cart.tsx component." But a source map for release v1.2.2 can't de-minify a stack trace from release v1.2.3—they're different builds.
When you upload source maps to LightTrace (via the Sentry CLI or API), include the release tag:
sentry-cli releases files upload-sourcemaps \
--release "web@$(git rev-parse --short HEAD)" \
./out
LightTrace then matches incoming stack traces by both the minified content AND the release, ensuring the right source map is applied. For the full setup—CI workflows, secrets management, and best practices for source map uploads—see source maps explained and uploading source maps.
If source maps and release tags don't align, you'll see cryptic minified stack traces even in LightTrace. This is the number one reason teams complain that "error tracking isn't helping"—they're sending unreadable traces because the source map upload release tag doesn't match the app's release tag.
What it unlocks
With releases tagged, the dashboard becomes your deploy audit trail.
Filter issues by release. Instead of "we have a TypeError," you see "we have a TypeError that first appeared in web@a1b2c3d." The first-seen release is your regression—the exact deploy that introduced the bug.
Detect regressions automatically. LightTrace watches for errors that reappear in a new release after being absent. If a TypeError was quiet for three versions, then re-appears in v1.4.0, LightTrace auto-reopens the issue with an alert: "This issue reappeared in a new release." No manual triage needed.
See crash-free rate by release. Instead of "we're at 99.5% crash-free," you see "v1.4.0 is at 98.2% crash-free, down from 99.7% in v1.3.9." A single metric—one version's crash-free rate—often tells you more than raw error counts. A version with a 2% drop is clearly broken; one with a 0.1% drop is probably fine.
Release health monitoring and auto-rollback triggers. Alert rules can fire based on release-specific thresholds: "if this release's crash-free rate drops below 95%, page on-call." Combined with email alerts, you catch broken deploys before they scale.
The consistency trap
Here's the gotcha: client and server must report the SAME release string, or your data splits in two. A common mistake is:
// ❌ WRONG: Server and client report different releases
// Client
Sentry.init({
release: `web@${process.env.NEXT_PUBLIC_APP_VERSION}`, // "web@a1b2c3d"
});
// Server
Sentry.init({
release: `api@${process.env.BACKEND_VERSION}`, // "api@9x8y7z"
});
Now an issue affecting both client and server shows up as two separate issues in the dashboard—one release per layer. You can't correlate across the stack, and release health metrics are split and confusing.
The fix: Use the same release tag everywhere.
// ✅ RIGHT: Client and server report the same release
const releaseVersion = process.env.NEXT_PUBLIC_APP_VERSION || "dev";
// Client
Sentry.init({
release: `myapp@${releaseVersion}`,
});
// Server
Sentry.init({
release: `myapp@${releaseVersion}`,
});
Use the same format (myapp@v1.2.3 or just v1.2.3) across your entire system. Everything—browser code, API routes, server components, workers—reports under one release per deploy.
Tagging environment alongside release
Release tracking without environment tagging pollutes your data. Your staging environment will throw test errors under the same release as production, and you'll confuse alert spikes.
Always tag environment when you init the SDK:
Sentry.init({
dsn: "https://<key>@light-trace.robomiri.com/1",
release: `web@${process.env.NEXT_PUBLIC_APP_VERSION}`,
environment: process.env.NODE_ENV || "development", // "production", "staging", "development"
});
On your dashboard, filter environment:production when checking release health, so preview deploys and local errors don't pollute production metrics. LightTrace's alert rules also support environment filters, so you only get paged on production regressions.
Start tracking errors in minutes
Tag your Next.js app with next_public_app_version, point the Sentry SDK at LightTrace, and see which deploy introduced every error — start free and get release health and crash-free tracking immediately.
Release tracking turns error monitoring from reactive firefighting into proactive regression detection. Every deploy is then an experiment with measurable results, and rolling back becomes a decision, not a panic. Wire it up once in your build, and every future version ships with full deployment visibility built in.