Browse docs
Source maps
Stack frames go from index-DLrL68bZ.js:1:10383 to components/HexEditor.tsx:142:18.
Production bundles are minified — every stack frame is a symbol like lt at .../index-DLrL68bZ.js:1:10383, useless for debugging. Upload your .js.map files at deploy and GlitchReplay renders the original location, tagged MAPPED on each resolved frame. The minified location is preserved as a tooltip fallback.
How it works
There are three moving parts. All three must be in place:
- Build emits maps — your bundler outputs
.js.mapfiles alongside.js. - Build uploads maps — a tiny script POSTs each map to
/api/{project_id}/sourcemaps. - Client tags events with the same release — so the resolver can find the right map at view time.
Step 1: emit source maps
| Build target | Config | Output |
|---|---|---|
| Next.js | productionBrowserSourceMaps: true in next.config.{js,mjs,ts} | .next/static/chunks/*.js.map |
| Vite | build.sourcemap: true in vite.config.ts | dist/assets/*.js.map (or dist/client/assets/ if you pass --outDir dist/client, common for SSR builds) |
| esbuild | sourcemap: true | <name>.js.map next to each bundle |
| Astro | vite: { build: { sourcemap: true } } | dist/_astro/*.js.map |
For Cloudflare Pages with @cloudflare/next-on-pages, maps land at .vercel/output/static/_next/static/chunks/. For OpenNext on Workers they stay at .next/static/chunks/.
Step 2: upload maps at deploy
Vendor scripts/upload-sourcemaps.mjs from the GlitchReplay repo into your site. It's pure Node, zero dependencies. Add a script entry and chain it after your build:
"scripts": {
"build": "next build && npm run upload:sourcemaps",
"upload:sourcemaps": "node scripts/upload-sourcemaps.mjs --dsn '<YOUR_DSN>' --dir .next/static/chunks --best-effort"
}For Vite, swap the --dir for your build output:
"scripts": {
"build": "vite build && npm run upload:sourcemaps",
"upload:sourcemaps": "node scripts/upload-sourcemaps.mjs --dsn '<YOUR_DSN>' --dir dist/assets --best-effort"
}Flags:
--dsn— your project DSN (or set$GLITCHREPLAY_DSN). The DSN public key is safe to commit.--dir— directory to walk for.js.mapfiles. Use the post-build chunks dir for your platform.--release— optional. Auto-detected from$WORKERS_CI_COMMIT_SHA,$CF_PAGES_COMMIT_SHA,$GITHUB_SHA,$CI_COMMIT_SHA, or$VERCEL_GIT_COMMIT_SHA(first 7 chars).--best-effort— recommended for CI. Logs warnings instead of failing the deploy if DSN/release are missing.
Step 3: tag events with the same release
The release string the upload script uses must match the release field on incoming events. Inject it at build time:
// next.config.mjs
const RELEASE =
process.env.WORKERS_CI_COMMIT_SHA?.slice(0, 7) ||
process.env.CF_PAGES_COMMIT_SHA?.slice(0, 7) ||
process.env.GITHUB_SHA?.slice(0, 7) ||
"dev";
export default {
productionBrowserSourceMaps: true,
env: { NEXT_PUBLIC_RELEASE: RELEASE },
};For Vite, inject via define so it lands on import.meta.env:
// vite.config.ts
const RELEASE =
process.env.WORKERS_CI_COMMIT_SHA?.slice(0, 7) ||
process.env.CF_PAGES_COMMIT_SHA?.slice(0, 7) ||
process.env.GITHUB_SHA?.slice(0, 7) ||
"dev";
export default defineConfig({
define: {
"import.meta.env.VITE_RELEASE": JSON.stringify(RELEASE),
},
build: { sourcemap: true },
});Then in Sentry.init (or any other SDK):
Sentry.init({
dsn: "<YOUR_DSN>",
// Next.js
release: process.env.NEXT_PUBLIC_RELEASE,
// Vite
// release: import.meta.env.VITE_RELEASE,
});Verifying it works
- Trigger a real error after a fresh deploy:
throw new Error("sourcemap test") - Open the issue in the dashboard.
- Stack frames should show original file paths with a
MAPPEDpill on each.
Troubleshooting
- Frames still minified — check the event payload for
release. If it's"dev"or missing, step 3 didn't take. - Maps uploaded, but no resolution — filenames must match by
basename. The resolver pairsbasename(frame.filename)against<release>/<basename>.map. URL prefixes are fine; a different hash in the filename means no map. - 10 MB limit per map file. The upload script warns and skips anything larger.
- Idempotent — re-uploading the same key overwrites silently, so you can re-run the upload script safely on retry.
Storage layout
Maps are stored in R2 at {project_id}/{release}/{basename}.map. Parsed maps are cached in-isolate (LRU 32) so hot issues don't re-parse on every render.
Next: enable session replay, or learn how the security signals are extracted from the same event stream.