Why your Sentry SDK isn't capturing source-mapped stack traces

The five most common reasons frames render as `<anonymous>` even after you've uploaded source maps — and the diagnostic checklist that finds the cause in under five minutes.

·
source-mapsdebugging

You just pushed a critical fix for a production crash. You open the dashboard to verify it, and what greets you is a sea of main.7a8f2.js and <anonymous> frames. You've checked everything: the .map files exist in your CI logs, the upload command returned a clean 200 OK, and yet the stack trace is unreadable. Welcome to Source Map Purgatory—the place where good developers go when a single character in a release string doesn't match.

Here's the thing almost nobody tells you: most source-map failures are not caused by a broken upload. The upload usually worked fine. The failure is an invisible metadata mismatch between the event your SDK emits at runtime and the artifacts sitting on the server. The fix isn't "re-read the docs"—it's a systematic diagnostic that walks the chain from SDK to CI/CD to server and finds the one broken link. This post gives you that diagnostic, in the order it actually fails.

The anatomy of a source map match

Symbolication is a handshake. When your error tracker receives a minified frame, it has to find the exact source map that corresponds to that exact file from that exact deploy. It does this by matching on three things: the release identifier, the optional dist tag, and the file's path or URL. If any of those don't line up between what the SDK reported and what you uploaded, the symbolicator gives up and you get <anonymous>.

How the server selects a map

Picture two records side by side. The SDK's event contains an abs_path like https://app.example.com/static/js/main.7a8f2.js and a release like my-app@1.4.2. The uploaded artifact has a file name and is filed under a release of its own. The server tries to reconcile the event's path and release against the artifact's. When they agree, you get processPayment (src/checkout/pay.ts:42:9). When they don't, you get nothing useful.

Why "anonymous" happens

<anonymous> is the symbolicator's way of saying "I located a frame but I could not map it back to a name." It found a stack position; it could not find the map—or the map it found didn't cover that position. That distinction matters, because it tells you the problem is in the matching, not in whether maps exist at all.

Reason 1: the release and dist mismatch (the #1 killer)

By a wide margin, the most common cause is that the SDK reports one version at runtime and the maps were uploaded under another. The two values are computed at different times by different processes, and they drift.

Case sensitivity and hidden whitespace

A classic offender is using $GITHUB_SHA for the release. During the build step you might capture the full 40-character SHA; at runtime your app might read a 7-character short SHA injected by a different tool. Or a trailing newline sneaks into an environment variable from a shell command. my-app@a1b2c3d and my-app@a1b2c3d4e5f6... are different strings, and the matcher treats them as such.

// Build step (CI) — uploads maps under the FULL sha
RELEASE="my-app@$(git rev-parse HEAD)"   // my-app@a1b2c3d4e5f6...
sentry-cli sourcemaps upload --release "$RELEASE" ./dist

// Runtime (app bundle) — reports the SHORT sha
Sentry.init({
  dsn: process.env.SENTRY_DSN,
  release: process.env.NEXT_PUBLIC_GIT_SHA, // "a1b2c3d" — no match!
});

The fix is to compute the release string once, export it as a single environment variable, and feed that same variable to both the upload command and the SDK init. Never derive it twice.

The dist tag

The dist tag disambiguates builds that share a release—for example, the same version shipped to web, iOS, and Android. It's powerful, but it's also a second value that has to match. If you upload maps with a dist but your SDK init omits it (or vice versa), the match fails even when the release is identical. Use dist deliberately, or not at all, and keep it symmetric on both sides.

Reason 2: the path-mapping hallucination

The second most common failure is a path mismatch. The SDK reports the URL of the script as the browser loaded it—https://app.example.com/static/js/main.js—but the source map's internal sources array (and the name the artifact is stored under) thinks the file is ~/main.js or /app/dist/main.js or app:///main.js. The server can't reconcile two different naming conventions for the same file.

abs_path vs. the sources array

The event's abs_path is whatever the runtime saw. The artifact's identity is whatever your build tool wrote. These diverge constantly because bundlers love prefixes—webpack://, ~/, absolute filesystem paths from the build container that don't exist on the CDN.

Using rewriteFrames to align them

The fix is to normalize the frames at SDK time so they match how the artifacts are stored. The rewriteFrames integration rewrites abs_path on every frame before the event leaves the browser.

import * as Sentry from "@sentry/browser";
import { RewriteFrames } from "@sentry/integrations";

Sentry.init({
  dsn: process.env.SENTRY_DSN,
  integrations: [
    new RewriteFrames({
      // Strip the host/path prefix so frames read "app:///main.js"
      iteratee(frame) {
        if (frame.filename) {
          const file = frame.filename.split("/").pop();
          frame.filename = "app:///" + file;
        }
        return frame;
      },
    }),
  ],
});

Upload your artifacts under the matching ~/ or app:/// prefix and the two sides finally agree.

Reason 3: build-tool sabotage

Sometimes the map is generated and matched correctly, but the original code was already stripped of function names before the map was ever written. The map faithfully points to a location—it just has no name to give you there.

keep_fnames and keep_classnames

Aggressive minifier settings can drop function and class names entirely. If Terser is configured with name-mangling that doesn't preserve identifiers, your map can resolve the line but still surface <anonymous> for the function. Setting keep_fnames: true and keep_classnames: true preserves them; the bundle-size cost is typically only 1–3%, which is a trade most teams happily make for readable traces.

Why devtool: source-map is mandatory

In Webpack, only the source-map devtool setting produces a separate, high-fidelity .map file suitable for production symbolication. eval-source-map and the other eval-* variants inline maps into eval() wrappers optimized for dev rebuild speed—they will not symbolicate correctly server-side. For production, source-map (or hidden-source-map if you don't want the sourceMappingURL comment) is the only correct choice.

Reason 4: the CI/CD race condition

Timing is the silent killer of rolling deploys. If your new app version goes live before its maps finish uploading, the first wave of errors arrives at the server with no artifacts to match against. Those events are permanently <anonymous>—re-uploading later doesn't re-symbolicate events that already failed.

The correct sequence

The order must be: build, then upload source maps, then deploy. Never deploy first.

# .github/workflows/deploy.yml (excerpt)
- name: Build
  run: npm run build

- name: Upload source maps  # BEFORE the app is live
  run: |
    sentry-cli sourcemaps inject ./dist
    sentry-cli sourcemaps upload --release "$RELEASE" ./dist

- name: Deploy  # only after maps are safely on the server
  run: npm run deploy

And avoid url_prefix unless you genuinely need it—it's another path-shaped value that has to match the runtime, and it's a frequent source of the Reason 2 mismatch above.

Reason 5: hidden network failures

If your provider fetches maps over the network rather than from uploaded artifacts, your own infrastructure can block it. Security headers, IP allowlists, or a firewall can return a 403 to the symbolicator's fetch even though the map loads fine in your browser. "Hidden" source maps stashed behind a VPN are the extreme case: invisible to attackers and equally invisible to your error tracker. Verify external accessibility directly:

# Run from outside your network, not your laptop on the VPN
curl -I https://app.example.com/static/js/main.7a8f2.js.map

A 200 from an external network means the symbolicator can reach it; a 403 or 404 means you've found your problem.

The 5-minute diagnostic checklist

When you see <anonymous>, run these in order and stop when one fails:

  • Check 1 — artifacts exist: sentry-cli releases files <version> list confirms the maps actually landed under that release.
  • Check 2 — release matches: open the raw JSON of the failing event and compare its release field, character for character, against the CLI output from Check 1.
  • Check 3 — map is reachable: validate the SourceMap header and the .map URL from an external network with curl.
  • Check 4 — explain the gap: sentry-cli sourcemaps explain <event-id> tells you exactly which of the above the matcher couldn't satisfy.

For the build-tool side of these failures, our walkthrough on why stack traces show minified function names covers generation in depth, and the source-maps documentation has the full reference. If you just need to read one trace right now, paste it into the deminifier tool with your .map file and skip the server entirely.

How GlitchReplay simplifies the mapping headache

A subtle reason teams end up in Purgatory is cost: on per-event platforms, some teams stop uploading maps for high-volume releases to save on processing and storage—see our breakdown of per-event pricing. GlitchReplay's flat rate removes that incentive entirely; there's no value-based dropping of source-map data and no per-event storage limit, so you upload every map for every deploy without watching a meter.

And because GlitchReplay is natively compatible with the Sentry SDK, every diagnostic in this post applies unchanged—the same release, dist, and rewriteFrames mechanics work, pointed at a flat-rate edge backend. Symbolicated traces become the bridge to your session replay, so you don't just read where the error happened—you watch it happen.

Stop squinting at minified stack traces. Fix the one broken link in the handshake, and consider a backend that encourages you to upload every map, every time.

Stop watching your error bill spike.

GlitchReplay is Sentry-SDK compatible, includes session replay and security signals, and never charges per event. Free to start, five minutes to first event.