# jso-symbolicate

Demangle obfuscated JavaScript stack traces using an identifier map produced by [JavaScript Obfuscator](https://www.javascriptobfuscator.com/). Pure JavaScript, zero dependencies, no network calls — the map lives on your machine, the stack trace lives on your machine, and the substitution happens locally.

Works alongside [`jso-protector`](https://javascriptobfuscator.com/docs/npmcli.aspx) (the CI/CD CLI) and the HTTP API.

## Why local symbolication

Stack traces frequently include customer data — file paths, user IDs in URLs, internal route names. Sending them to a third-party symbolicator service is a privacy regression. `jso-symbolicate` runs entirely on your machine. The only thing it reads is the identifier map you already have from your obfuscation run.

## Quick start

```bash
npm install --save-dev https://javascriptobfuscator.com/download/jso-symbolicate/jso-symbolicate-0.5.0.tgz

# Symbolicate a stack trace piped in on stdin:
cat crash.txt | npx jso-symbolicate --map build-2026-05-19.report.json

# Or read from a file:
npx jso-symbolicate --map build-2026-05-19.report.json --stack crash.txt

# Keep mangled names with a /* original */ comment instead of replacing:
npx jso-symbolicate --map ./global.map.txt --map ./member.map.txt --annotate < crash.txt

# Machine-readable output (good for CI):
npx jso-symbolicate --map ./report.json --json < crash.txt

# Process captured error-reporter event JSON locally:
npx jso-symbolicate --map ./report.json --event sentry --event-file sentry-event.json > sentry-event.demangled.json
```

`jso-symbolicate` is distributed as a versioned direct download. It is not
currently published on the npm registry; the unqualified command
`npm install jso-symbolicate` will fail.

## Supported map formats

`jso-symbolicate` accepts any of these in `--map`:

| Source | Format |
|---|---|
| JSO HTTP API response JSON | `{ Report: { GlobalIdentifierMap: [...], MemberIdentifierMap: [...], GlobalIdentifierMapBlock: "...", MemberIdentifierMapBlock: "..." } }` |
| Bare report | `{ GlobalIdentifierMap: [...], MemberIdentifierMap: [...] }` |
| Array form | `[{ Mangled: "_0x2a3b", Original: "calculateLicenseHash" }, ...]` |
| Object form | `{ "_0x2a3b": "calculateLicenseHash", ... }` |
| Block text | `_0x2a3b=calculateLicenseHash\n_0x4f1c=verifySignature\n...` |

Pass `--map` multiple times when global and member maps live in separate files; they are merged into a single lookup.

## Programmatic use

```js
const { buildLookup, demangleStack, demangleStackWithStats } = require("jso-symbolicate");

const lookup = buildLookup(
    report.GlobalIdentifierMap,
    report.MemberIdentifierMap
);

// Replace mangled tokens in place:
const readable = demangleStack(rawStack, lookup);

// Same but with statistics:
const { stack, hits, perLine } = demangleStackWithStats(rawStack, lookup);
console.log(`Rewrote ${hits} tokens across ${perLine.length} lines.`);
```

## Detecting wrong-map cases

A stack trace symbolicated against the wrong build's map produces zero `hits`. CI integration tip: fail the symbolication step if `hits === 0` while `mapEntries > 0` — that almost always means the map and the stack belong to different builds.

```bash
npx jso-symbolicate --map ./report.json --json --stack crash.txt | \
  node -e 'const j = JSON.parse(require("fs").readFileSync(0,"utf8")); if (j.hits === 0 && j.mapEntries > 0) { console.error("Map/stack mismatch — wrong build?"); process.exit(1); }'
```

## Symbolicate captured event JSON

When a support team already has a raw error event, the CLI can run the same
reporter-specific transforms locally and emit demangled JSON. This gives teams
a source-map-style review workflow without uploading Source Map v3 files or
changing runtime code first.

```bash
npx jso-symbolicate --map ./report.json --event sentry --event-file sentry-event.json > sentry-event.demangled.json
cat datadog-event.json | npx jso-symbolicate --map ./report.json --event datadog --json > datadog-event.demangled.json
```

Supported event shapes are `sentry`, `bugsnag`, `rollbar`, `datadog`,
`honeybadger`, `raygun`, `airbrake`, and `appsignal`. For Sentry, add
`--preserve-original` to attach the mangled event under `extra` for forensic
comparison.

Programmatic scripts can call the same dispatcher:

```js
const { symbolicateEvent } = require("jso-symbolicate/event");
const event = symbolicateEvent("sentry", rawEvent, lookup, { preserveOriginal: true });
```

In event mode, `--json` emits `{ reporter, event, hits, mapEntries }` so triage
scripts can detect wrong-map cases the same way stack mode does.

## Integration with error reporters

### Sentry

JSO's identifier map is name-only (no line/column data), so Sentry's built-in Source-Map symbolication can't auto-pick it up. The supported pattern is a `beforeSend` Event Processor that demangles the event in-place before it ships:

```js
const Sentry = require("@sentry/browser");
const { buildLookup } = require("jso-symbolicate");
const { createSentryEventProcessor } = require("jso-symbolicate/sentry");

const report = require("./jso-report.json"); // saved by jso-protector --report
const lookup = buildLookup(
    report.Report.GlobalIdentifierMap,
    report.Report.MemberIdentifierMap
);

Sentry.init({
    dsn: "...",
    beforeSend: createSentryEventProcessor(lookup)
});
```

The processor walks `event.message`, every `event.exception.values[].stacktrace.frames[]`, every `event.threads.values[].stacktrace.frames[]`, and the `breadcrumbs[]` array, demangling function names, modules, and inline source snippets. Events arrive in Sentry already readable.

Pass `{ preserveOriginal: true }` as a second argument to keep the un-demangled stack as a Sentry extra (useful when you want forensic comparison).

### Bugsnag

Same pattern as Sentry, wired through `onError`:

```js
const Bugsnag = require("@bugsnag/js");
const { buildLookup } = require("jso-symbolicate");
const { createBugsnagOnError } = require("jso-symbolicate/bugsnag");

const report = require("./jso-report.json");
const lookup = buildLookup(report.Report.GlobalIdentifierMap, report.Report.MemberIdentifierMap);

Bugsnag.start({
    apiKey: "...",
    onError: createBugsnagOnError(lookup)
});
```

### Rollbar

```js
const Rollbar = require("rollbar");
const { buildLookup } = require("jso-symbolicate");
const { createRollbarTransform } = require("jso-symbolicate/rollbar");

const report = require("./jso-report.json");
const lookup = buildLookup(report.Report.GlobalIdentifierMap, report.Report.MemberIdentifierMap);

const rollbar = new Rollbar({
    accessToken: "...",
    transform: createRollbarTransform(lookup)
});
```

The transform walks `payload.data.body.trace`, `trace_chain[]`, `payload.data.body.message`, and the breadcrumb `telemetry[]` array. Transform errors are swallowed so a broken lookup never blocks crash reporting.

### Datadog Browser RUM / Logs

```js
import { datadogRum } from "@datadog/browser-rum";
const { buildLookup } = require("jso-symbolicate");
const { createDatadogBeforeSend } = require("jso-symbolicate/datadog");

const lookup = buildLookup(window.__JSO_REPORT__.Report.GlobalIdentifierMap, window.__JSO_REPORT__.Report.MemberIdentifierMap);

datadogRum.init({
    applicationId: "...",
    clientToken: "...",
    beforeSend: createDatadogBeforeSend(lookup)
});
```

Same pattern works for `datadogLogs.init({ beforeSend: ... })`. The callback demangles `event.error.message`, `event.error.type`, `event.error.stack`, action names, and Log message bodies.

### Honeybadger

```js
const Honeybadger = require("@honeybadger-io/js");
const { buildLookup } = require("jso-symbolicate");
const { createHoneybadgerBeforeNotify } = require("jso-symbolicate/honeybadger");

const report = require("./jso-report.json");
const lookup = buildLookup(report.Report.GlobalIdentifierMap, report.Report.MemberIdentifierMap);

Honeybadger.configure({
    apiKey: "...",
    beforeNotify: createHoneybadgerBeforeNotify(lookup)
});
```

### Raygun (Raygun4JS / Raygun4Node)

```js
const rg4js = require("raygun4js");
const { buildLookup } = require("jso-symbolicate");
const { createRaygunBeforeSend } = require("jso-symbolicate/raygun");

const report = require("./jso-report.json");
const lookup = buildLookup(report.Report.GlobalIdentifierMap, report.Report.MemberIdentifierMap);

rg4js("onBeforeSend", createRaygunBeforeSend(lookup));
```

### Other reporters (AppSignal, Airbrake, custom)

Most reporters expose a similar "transform the event before sending" hook. Pass the raw `error.stack` string through `demangleStack(stack, lookup)` from the main entry point and substitute the result into whichever field your reporter exposes.

### Source Map v3

A Source Map v3 with line/column mappings would let Sentry / Datadog / Bugsnag auto-symbolicate frame positions, but JSO's current API response records `{mangled, original}` name pairs, not generated-code positions. Use the runtime event processors or `jso-symbolicate --event <reporter>` when you want readable reporter events without source-map upload or position-aware map generation.

## License

UNLICENSED. Provided as a free companion to the JSO service. The substitution algorithm has no JSO-specific dependencies; it would work with any pre-shared {mangled, original} dictionary.
