Documentation

Guides for protecting production JavaScript

Reference guides for release workflows, command-line usage, cross-file protections, and the desktop app.

Inside The Docs

Practical guides, not placeholder pages.

How-to guides Start with release sequencing and command-line usage, then move into feature-specific references.
Advanced protection Browse cross-file controls like Replace Globals and Protect Members when a build spans multiple scripts.

AI-augmented production triage

  • jso-symbolicate · /v1/ai/explain-error
  • Sentry, Bugsnag, Rollbar, Datadog, Honeybadger, Raygun, Airbrake, AppSignal
  • jso-symbolicate ships today · explain-error in preview, full version 2026-Q3

Production crashes in protected JavaScript travel through three stages before someone can fix them: (1) the runtime catches the error and the reporter SDK ships it to Sentry / Bugsnag / Rollbar / etc, (2) the function names in the stack are mangled and need to be demangled, (3) the demangled stack still doesn't tell you which JSO transform caused the issue. The two existing JSO products — jso-symbolicate and the new /v1/ai/explain-error endpoint — cover stages 2 and 3. This page shows the full composition.

The end-to-end pipeline

  1. Crash happens in production. The protected bundle throws. The error reporter SDK (Sentry / Bugsnag / etc) catches it and ships the stack trace to the customer’s aggregator.
  2. jso-symbolicate demangles the stack. Either inline via the reporter’s beforeSend hook (one of eight first-class integrations) or offline via the CLI when triaging from a saved report. The mangled function names become the original names from the source.
  3. /v1/ai/explain-error diagnoses the JSO transform. The demangled error string goes to the explain-error endpoint, which returns the most likely JSO transform that caused the issue, a concrete fix, and a docs link.
  4. You apply the fix and ship. Add the function to VariableExclusion, toggle FlatTransform: false on the affected file, or whatever the diagnosis recommends.

Inline composition: reporter beforeSend → AI diagnosis

The cleanest path is to have the reporter’s beforeSend hook both demangle the stack and annotate the event with the AI’s diagnosis. When the event lands in Sentry / Bugsnag / Datadog, the triage engineer reads not just “calculateLicenseHash is not a function” but also “Name Mangling renamed this; add calculateLicenseHash to VariableExclusion.”

Sketch (Sentry browser SDK):

import * as Sentry from "@sentry/browser";
import { buildLookup } from "jso-symbolicate";
import { createSentryEventProcessor } from "jso-symbolicate/sentry";

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

const demangle = createSentryEventProcessor(lookup);

Sentry.init({
    dsn: "...",
    release: window.__JSO_BUILD_ID__,
    beforeSend: async (event) => {
        // 1. Demangle the stack in-place.
        const demangled = demangle(event);

        // 2. Ask /v1/ai/explain-error what JSO transform likely caused this.
        //    Server-side proxy recommended in production — don't expose your
        //    JSO API key in the browser.
        try {
            const resp = await fetch("/api/jso/explain-error", {
                method: "POST",
                headers: { "Content-Type": "application/json" },
                body: JSON.stringify({
                    error: demangled.exception.values[0].value
                            + " " + (demangled.exception.values[0].type || "")
                }),
            });
            const json = await resp.json();
            if (json.ok && json.explanation) {
                demangled.tags = demangled.tags || {};
                demangled.tags.jso_diagnosis_transform = json.explanation.transform;
                demangled.tags.jso_diagnosis_confidence = json.explanation.confidence;
                demangled.extra = demangled.extra || {};
                demangled.extra.jso_diagnosis = json.explanation;
            }
        } catch (_) { /* swallow — never block crash reporting */ }

        return demangled;
    },
});

The Sentry event that lands in the UI now carries two extra pieces of information:

  • The jso_diagnosis_transform tag — filterable / searchable. Find “all Name Mangling-caused crashes this week.”
  • The full jso_diagnosis extra — cause, transform, confidence, explanation, fix, docs URL. The triage engineer sees a recommended fix without leaving Sentry.

Offline composition: pull a saved report, run both CLIs

When the crash is in a saved log file or a Sentry export rather than a live event stream:

# 1. Demangle.
npx jso-symbolicate --map dist-protected/jso-report.json --stack crash.txt > crash-demangled.txt

# 2. Diagnose. Pull the top error line, send to the explain-error endpoint.
ERROR=$(head -1 crash-demangled.txt)
curl -sS -X POST https://www.javascriptobfuscator.com/v1/ai/explain-error.ashx \
    -H "Content-Type: application/json" \
    -d "{
      \"APIKey\":   \"$JSO_API_KEY\",
      \"APIPwd\":   \"$JSO_API_PASSWORD\",
      \"error\":    $(jq -Rs . <<< \"$ERROR\")
    }" | jq '.explanation'

This is the path support uses when a customer pastes a stack trace in a ticket. Two commands, no service to keep running.

Why the AI endpoint, not just static rules in the client

Two reasons the JSO-side endpoint earns the network round-trip:

  1. The rule table stays in sync. The patterns the explain-error endpoint matches are JSO-engineering-team-curated and get updated when transforms change. A client-side static table written today goes stale the first time JSO ships a new transform.
  2. LLM upgrade path. The same endpoint URL transitions from rule-based to Claude-backed at 2026-Q3 GA without any customer-side change. Sentry events tagged with jso_diagnosis_* today and the same events tagged with jso_diagnosis_* next year are directly comparable, but the answers in 2027 will reason about novel error patterns the rules don’t cover.

If your environment cannot make outbound HTTP from the reporter beforeSend hook, the offline composition path covers the gap — nothing about the architecture requires inline calls.

Quota and cost

Each explain-error call counts 1 AI action against the JSO AI subscription tier (see pricing). When the action quota is hit, the endpoint returns ok: false with error: "quota_exhausted" — your beforeSend hook’s try/catch handles it gracefully and the event still ships with the demangled stack, just without the AI diagnosis tag. The point: the AI augmentation is additive, not gating.

For a heavy-traffic site, route AI diagnosis only to a sampled subset of events (e.g. 10% of unhandled exceptions, 100% of high-severity Sentry events) to keep the AI quota usage proportional to debugging value rather than total crash volume.

The eight reporters with the explain-error pattern wired in

Reporterjso-symbolicate integrationexplain-error hook
Sentry createSentryEventProcessor in beforeSend Inline (above)
Bugsnag createBugsnagOnError in onError Inline in the same callback
Rollbar createRollbarTransform as transform Inline in the same transform
Datadog Browser RUM / LogscreateDatadogBeforeSend in beforeSend Inline in the same callback
Honeybadger createHoneybadgerBeforeNotify Inline in the same callback
Raygun createRaygunBeforeSend via rg4js("onBeforeSend", ...) Inline in the same callback
Airbrake createAirbrakeFilter via addFilter Inline in the same callback
AppSignal createAppSignalDecorator via addDecorator Inline in the same callback

Every reporter exposes a single beforeSend-style hook; the composition pattern is the same in all eight. Pick your reporter, drop in the matching jso-symbolicate sub-export, and add the fetch-to-/v1/ai/explain-error call inside the same callback.

What this gets you in practice

Concrete before/after for the same crash:

Before (reporter raw):

TypeError: _0xa3f.b is not a function
    at _0x1c2 (https://app.example.com/protected.js:1:8742)
    at _0x4f9 (https://app.example.com/protected.js:1:13205)

After (with the composition wired):

TypeError: calculateLicenseHash is not a function
    at boot (https://app.example.com/protected.js:1:8742)
    at runStartup (https://app.example.com/protected.js:1:13205)

jso_diagnosis_transform = "Name Mangling"
jso_diagnosis_confidence = "high"

jso_diagnosis (extra):
  cause:        "name-mangling"
  explanation:  "A renamed identifier was called as a function by its original name.
                 Typically external code (a framework, an HTML inline handler,
                 a window-scoped consumer) references the symbol by its source-level name."
  fix:          "Add the function name to VariableExclusion in your jso.config.
                 Run jso-protector --dry-run --json to confirm the function got included."
  docsUrl:      "/Docs/VariableExclusionList.aspx"

That’s the value the composition delivers: from “_0xa3f.b” to “here’s the JSO option to flip” with zero engineer-driven investigation.