Skip to content

feat(wasm-debug-files): Add prepare command for WASM debug setup - #1572

Draft
d2anamaria wants to merge 7 commits into
mainfrom
ana/feat/wasm/debug-file-pipeline
Draft

feat(wasm-debug-files): Add prepare command for WASM debug setup#1572
d2anamaria wants to merge 7 commits into
mainfrom
ana/feat/wasm/debug-file-pipeline

Conversation

@d2anamaria

Copy link
Copy Markdown

Problem

Preparing WebAssembly for Sentry takes two tools today. You run wasm-split to inject a build_id and pull DWARF out into a companion file, then run sentry debug-files upload --type wasm on that companion. Nothing tells you when a module was built without usable debug info, so the mistake surfaces later as an unsymbolicated stack trace.

Solution

sentry debug-files prepare <path>... does both steps in one command.

sentry debug-files prepare ./dist

It scans the given files and directories for .wasm modules, splits the ones carrying inline DWARF, and uploads the companions. Org and project are auto-detected from DSN, env vars, or config defaults. --dry-run and --no-upload need no credentials.

What it does per module

For a module with inline DWARF:

  1. Inject a build_id if it has none.
  2. Write a *.debug.wasm companion keeping every section, including Code and DWARF.
  3. Strip the .debug_* sections from the deployable module, in place.
  4. Point the deployable at the companion via external_debug_info.
  5. Upload the companion.

The deployable keeps its original path, so your build artifact does not move. Both files carry the same build_id, which is how Sentry matches a stack frame to its debug file. The companion keeps the Code section because DWARF addresses are relative to it.

A module that cannot be split is still stamped with a build_id, then reported with a warning. Stamping matters: without a build_id a module can never be symbolicated, not even from a debug file uploaded later. Nothing is uploaded for it.

Outcome Meaning Uploaded
Split Had DWARF; companion written Yes
Already prepared Companion with matching build_id already exists Yes
Would split --dry-run preview No
Skipped Not splittable; stamped, warning printed No

Warnings

A module without usable debug info does not fail the run unless --require-dwarf is set. The warning says why it was skipped:

  • no line-level symbolication (name/symtab only)
  • no debug information; rebuild with DWARF (Emscripten -g, wasm-pack dwarf-debug-info)
  • already stripped (build_id present, no debug sections); splitting would produce a useless companion
  • has external_debug_info but no local companion with matching build_id

Re-running is safe. An already-prepared pair is detected and left alone rather than overwritten with an empty companion, and a module keeps the build_id it was stamped with on the first run.

Options

Flag Effect
--dry-run Classify only; write nothing, upload nothing
--no-upload Split, but skip the upload
--require-dwarf Fail if any scanned module lacks DWARF
--out-dir <DIR> Put companions here; deployables are always stripped in place
--strip-names Also drop the name section from split deployables; the companion keeps it
--build-id <UUID> Use a fixed build id instead of a random one, for modules that have none
--include-sources Also upload a source bundle per companion
--wait / --wait-for <SECS> Wait for server-side processing
--ignore <GLOB> / --ignore-file <FILE> Skip paths while scanning

--require-dwarf is the CI guard, and it runs before anything is uploaded, so a build missing debug info fails without pushing files first. A module pointing at an external companion counts as having DWARF, so a dangling pointer does not fail the build.

--ignore globs are relative to the tree you point at: --ignore 'vendor/**' with prepare ./dist means ./dist/vendor.

Scanning rules

Directories are walked recursively. *.debug.wasm files are skipped, since they are outputs of an earlier run. Naming a non-.wasm file directly is an error; a directory with no modules is just an empty scan.

Automation

--json reports the outcome per module, so a build script can act on the classification instead of grepping logs:

{
  "org": "my-org",
  "project": "my-project",
  "uploaded": true,
  "filesUploaded": 1,
  "modules": [
    {
      "path": "dist/app.wasm",
      "action": "split",
      "quality": "dwarf",
      "buildId": "",
      "companion": "dist/app.debug.wasm"
    }
  ]
}

The command exits non-zero when --require-dwarf fails, and when a companion fails server-side processing under --wait.

Relationship to wasm-split

The split follows the same algorithm as Symbolicator's wasm-split, and custom sections follow the WebAssembly tool conventions, so companions are meant to be interchangeable in Sentry. It is a reimplementation, not a shared codepath: wasm-split is a binary-only Rust crate, so this ships its own minimal module reader. Only the module envelope is parsed and section payloads are re-emitted byte for byte, so sections the reader does not know about survive untouched.

Limitations

  • A module without DWARF gets a build_id but no debug file. Rebuild with DWARF and re-run to get line-level frames; the build_id is preserved.
  • external_debug_info records the companion filename only. There is no equivalent of wasm-split --external-dwarf-url. Sentry resolves by build_id, so this does not affect symbolication.
  • Splitting always strips. There is no mode that writes a companion and leaves the deployable intact.
  • With --out-dir, re-running without the same --out-dir will not find the companion and reports a dangling reference.

- Read and write WASM modules without dropping unknown sections
- Classify debug quality: dwarf, symtab, none, or external companion
- Split DWARF like wasm-split: companion keeps Code, deploy file loses .debug_*
- Inject build_id when missing; detect already-prepared pairs
- Stamp skipped modules with build_id when they have none
- Add unit and property tests
- Scan for .wasm files, split DWARF modules, upload companions to Sentry
- Flags: dry-run, no-upload, require-dwarf, out-dir, strip-names, build-id
- Skip symtab and no-debug modules with warnings; do not fail the whole run
- Human output: one block per file, yellow warnings, full paths
- Add command tests
- Add prepare examples and notes to debug-files docs
- Update agent skill reference for debug-files prepare
@vercel

vercel Bot commented Sep 10, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
cli Ready Ready Preview Sep 11, 2026 9:44am UTC
1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Sep 11, 2026 9:44am UTC

Request Review

@d2anamaria
d2anamaria marked this pull request as draft September 10, 2026 10:14
// Write the companion first: if the process dies between the two writes, an
// orphan companion is recoverable, whereas a stripped module whose DWARF was
// never saved anywhere is not.
await writeFile(expectedCompanion, split.companion);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: The prepareWasmFile function does not create the output directory specified by --out-dir before attempting to write files, which will cause a crash if the directory does not exist.
Severity: HIGH

Suggested Fix

Before calling writeFile, ensure the parent directory exists by adding a call like await mkdir(dirname(expectedCompanion), { recursive: true }). This pattern is already used in other parts of the codebase like bundle-sources.ts.

Prompt for AI Agent
Review the code at the location below. A potential bug has been identified by an AI
agent. Verify if this is a real issue. If it is, propose a fix; if not, explain why it's
not valid.

Location: packages/cli/src/lib/wasm/prepare.ts#L590

Potential issue: The `prepareWasmFile` function attempts to write a companion file using
`writeFile` to a path derived from the `--out-dir` option. However, Node.js's
`fs/promises.writeFile` does not create parent directories by default. If a user
specifies an output directory that does not yet exist, the operation will fail with an
`ENOENT` error. This is a common use case for a CLI tool that is expected to create its
own output directories.

Did we get this right? 👍 / 👎 to inform future reviews.

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8c8468e. Configure here.

const name = basename(wasmPath).replace(WASM_EXTENSION, "");
const fileName = `${name}${COMPANION_SUFFIX}`;
return join(outDir ?? dirname(wasmPath), fileName);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--out-dir overwrites same-named companions

High Severity

--out-dir names every companion from the module basename alone, so a recursive scan that hits two app.wasm files writes the same app.debug.wasm twice. The first module is stripped in place, then its companion is overwritten, so that DWARF is gone from both the deployable and disk and later runs cannot recover it.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c8468e. Configure here.

// Write the companion first: if the process dies between the two writes, an
// orphan companion is recoverable, whereas a stripped module whose DWARF was
// never saved anywhere is not.
await writeFile(expectedCompanion, split.companion);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing --out-dir directory creation

Medium Severity

--out-dir writes companions with writeFile and never creates the directory. writeFile does not create parents, so the documented prepare ./dist --out-dir ./symbols throws ENOENT unless ./symbols already exists. The error is uncaught and aborts the run after any earlier in-place stamps.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c8468e. Configure here.

}

return {
hint: `Uploaded ${uploads.length} debug companion(s) to ${params.org}/${params.project}`,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Source bundles counted as companions

Low Severity

filesUploaded is uploads.length minus failures, and --include-sources appends source-bundle DIFs into that same list. Human output then reports those extras as companions, so one companion plus one source bundle is shown as Uploaded 2 companions.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 8c8468e. Configure here.

- Fail --require-dwarf when skipped modules have unreachable external_debug_info
- Clarify hasDwarfQuality as build classification, not companion reachability
- Add recommendation on symtab and none skips to verify DWARF build flags
- Render skipped modules with a red header and cyan Recommendation line
- Export lacksDwarf and cover dangling-pointer gate behavior in tests
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 10, 2026 14:00 Inactive
@d2anamaria

Copy link
Copy Markdown
Author

A deploy .wasm can end up with external_debug_info (pointer to companion) but no build_id when the module was split outside wasm-split (custom script/tool) and the build never stamped ids (no --build-id / equivalent at compile). The companion may still be on disk with its own id. Today prepare skips with a warning and stamps a new random id on the deploy module only, which breaks the pair permanently. Would it be safe to treat this as repairable — read the companion's id (or generate one and write it to both), instead of skipping upload?

@BYK

BYK commented Sep 10, 2026

Copy link
Copy Markdown
Member

Would it be safe to treat this as repairable — read the companion's id (or generate one and write it to both), instead of skipping upload?

Sounds like we should, yes. Also a simple warning in this case sounds like the wrong action as we are essentially uploading broken stuff?

- Treat all `none` quality skips as missing DWARF, not already stripped
- Recommend verifying DWARF build flags for every `none` skip, with or without build_id
- Already-prepared modules return before skipWarning; this only affects skipped files
- Remove misleading already-stripped message for nosym/sourcemap-only modules
- Repair a module carrying external_debug_info but no build_id instead of
  stamping a random id that can never match its companion
- Adopt the companion's build_id, or stamp one shared UUID into both files
- Report the pair as already-prepared so the companion gets uploaded
- Write the companion first so a crash stays repairable on the next run
- Keep the existing skip when the companion is remote, missing, or has no DWARF
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants