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:

  1. Build emits maps — your bundler outputs .js.map files alongside .js.
  2. Build uploads maps — a tiny script POSTs each map to /api/{project_id}/sourcemaps.
  3. Client tags events with the same release — so the resolver can find the right map at view time.

Step 1: emit source maps

Build targetConfigOutput
Next.jsproductionBrowserSourceMaps: true in next.config.{js,mjs,ts}.next/static/chunks/*.js.map
Vitebuild.sourcemap: true in vite.config.tsdist/assets/*.js.map (or dist/client/assets/ if you pass --outDir dist/client, common for SSR builds)
esbuildsourcemap: true<name>.js.map next to each bundle
Astrovite: { 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.map files. 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

  1. Trigger a real error after a fresh deploy: throw new Error("sourcemap test")
  2. Open the issue in the dashboard.
  3. Stack frames should show original file paths with a MAPPED pill 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 pairs basename(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.