Operations

How to debug obfuscated JavaScript in production

The first protected release always ends the same way. Something throws, the error tracker fills with TypeError at _0x2a3b (app.js:1:48211), and someone asks the reasonable question: can we just ship the source map? No — and you do not need to. There is a workflow for this, it takes about fifteen minutes to set up once, and it keeps the protection intact.

Why not just publish the source map

A source map is a complete key to the build. It carries the original filenames, the original identifiers, and in the common configuration the original source text itself. Publishing one next to a protected bundle means anyone who requests app.js.map gets back exactly what you paid to protect — and this is not hypothetical, it is the single most common way protected source leaks.

Which is why protected release artifacts should not carry maps at all. The npm CLI excludes source maps by default and the bundler plugins strip stale JavaScript source maps from the output. Keep that behavior unless a separate secure release step owns map handling.

The identifier map: a key you keep

Every protection run returns a map recording which generated name corresponds to which original symbol. Unlike a source map, it is a small, private artifact that never goes near a browser. In the HTTP API response it is Report.GlobalIdentifierMap and Report.MemberIdentifierMap. From the npm CLI, pass --report ./out.report.json and it is written for you.

Store one map per build, next to the artifacts it describes, using whatever release layout you already keep:

release/
  build-2026-07-27/
    app.protected.js
    app.protected.js.report.json      <-- the identifier map
    SHA256SUMS

The discipline that matters: maps are per build. A trace from last Tuesday’s release needs last Tuesday’s map. Tag them with the same build identifier your error reporter records as the release, so the lookup is mechanical rather than archaeological.

Translating a trace

jso-symbolicate substitutes generated names back to readable ones. It is pure JavaScript, has zero dependencies, and makes no network calls — which is the point, because stack traces routinely contain file paths, user identifiers inside URLs, and internal route names that have no business being posted to a third-party symbolication service.

npm install --save-dev jso-symbolicate

# straight from a pasted crash
cat crash.txt | npx jso-symbolicate --map build-2026-07-27.report.json

# keep the generated name alongside the original
npx jso-symbolicate --map ./report.json --annotate < crash.txt

# machine-readable, for CI
npx jso-symbolicate --map ./report.json --json < crash.txt

It accepts the API response JSON, a bare report, an array or object of pairs, or plain block text, and --map can be passed more than once when the global and member maps live in separate files. If you would rather see it work before wiring anything up, the interactive symbolicate demo does the substitution client-side in your browser.

Wiring it into your error reporter

The pattern is the same for every tracker: let the raw event arrive, translate on your side. Export the captured event JSON and run it through the matching adapter:

npx jso-symbolicate --map ./report.json \
  --event sentry --event-file sentry-event.json \
  > sentry-event.demangled.json

Adapters ship for Sentry, Datadog, Rollbar, Bugsnag, Airbrake, Honeybadger, Raygun and AppSignal. Nothing about the tracker configuration changes — you are not asking it to decode anything, you are decoding after the fact with a key it never sees.

Four rules that keep this working

  • Never serve the map. Not from your origin, not from a CDN, not in a public bucket. A map reachable by URL is a source map with extra steps.
  • Record the build identifier in the client. A trace you cannot pair with a build is a trace you cannot translate.
  • Keep maps as long as you keep releases. Deleting the map for a version still running in the field means those crashes stay unreadable forever.
  • Treat the map like a credential. Same storage, same access control, same rotation thinking as anything else that would embarrass you in public.

Debugging before you get to production

Symbolication is for the field. Most protected-build problems are not field problems at all — they are configuration problems that show up on the first smoke test of the protected artifact. If your build is failing rather than merely unreadable, start with the six causes of obfuscated code not working; if the protected output is unreadable but functionally correct, that is the system doing its job, and this page is your workflow.

Related reading