Skip to content

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

Open
d2anamaria wants to merge 11 commits into
mainfrom
ana/feat/wasm/debug-file-pipeline
Open

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

Conversation

@d2anamaria

@d2anamaria d2anamaria commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

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 exactly one module that has 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

sentry wasm-split splits one module and stops there. This command is the pipeline around it: it
scans directories, classifies each module's debug quality, skips what it should not touch, and
uploads the companions. The split itself is the same splitWasm call.

Elsewhere the CLI reads debug files through @sentry/symbolic that wrapper (only reads).
This command rewrites modules: it injects build_id, strips .debug_*, and adds
external_debug_info. So it parses sections directly.

That is also why the two disagree on build_id. This command follows the tool convention;
symbolic reads it one byte shifted. The id sent here is advisory — the server re-parses the module
and stores its own key — so nothing breaks today. See
getsentry/symbolic#1069.

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. prepare does not expose wasm-split's --external-dwarf-url; reach for sentry wasm-split if you need a custom 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.

@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 18, 2026 12:45pm UTC
1 Skipped Deployment
Project Deployment Actions Updated
sentry-local Skipped Skipped Sep 18, 2026 12:45pm UTC

Request Review

@d2anamaria
d2anamaria marked this pull request as draft September 10, 2026 10:14
Comment thread packages/cli/src/lib/wasm/prepare.ts Outdated

@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.

Stale Bugbot comment from a previous run.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

not sure what’s the best solution for this.

  • remove flag, companions will always sit in the deploy directory
  • name by build_id, filename stops being human recognizable
  • another?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

name by build_id, filename stops being human recognizable

I think this makes the most sense.

Comment thread packages/cli/src/lib/wasm/prepare.ts Outdated
Comment thread packages/cli/src/commands/debug-files/prepare.ts Outdated
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 10, 2026 14:00 Inactive
@d2anamaria

Copy link
Copy Markdown
Contributor 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?

Comment thread packages/cli/src/lib/wasm/prepare.ts
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 15, 2026 12:43 Inactive
@d2anamaria
d2anamaria force-pushed the ana/feat/wasm/debug-file-pipeline branch from 344b5b7 to cf5ae11 Compare September 16, 2026 12:07
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 16, 2026 12:07 Inactive
@d2anamaria
d2anamaria changed the base branch from main to ana/feat/wasm/port-wasm-split September 16, 2026 12:10
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 16, 2026 12:28 Inactive
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 16, 2026 13:12 Inactive
@d2anamaria
d2anamaria force-pushed the ana/feat/wasm/port-wasm-split branch 4 times, most recently from 0fc4d8f to a408240 Compare September 16, 2026 13:47
@vercel
vercel Bot temporarily deployed to Preview – sentry-local September 16, 2026 14:44 Inactive
- Guard the inspectWasm build_id survey with ??= so a later malformed
  build_id section cannot erase an id already found, matching
  buildIdFromSections and the Rust tool's find_map
- Intersect PrepareFlags and UploadFlags with WaitFlags instead of
  restating --wait / --wait-for in each command
- Drop the orphaned resolveWaitMode JSDoc left behind in prepare.ts,
  which was nesting into the resolveBuildId comment block

@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 1 potential issue.

There are 4 total unresolved issues (including 3 from previous reviews).

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 9025888. Configure here.

isWasmPath(path) &&
!isDebugCompanionPath(path) &&
!isIgnored(path, ignoreMatchers, paths)
);

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.

Scan splits custom-named companions

High Severity

Directory scans only skip files named *.debug.wasm. A companion with any other name is treated as a deployable module, so prepare strips its DWARF in place. That destroys the pair repairUnpairedCompanion was added to fix, and later upload can send a companion that no longer contains debug info.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 9025888. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This would require another directory pass to parse every candidate and collect which paths are pointed at by someone else's external_debug_info, and exclude them from the next scan. Is it a too narrow edge case to add this extra pass&parse?
This solution only helps when the deployable is in the same scan and still carries its pointer, so custom-named companions sitting alone still can't be detected.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not fully familiar with the logic but why can we not build an in-memory tree as we do the first scan and use that later on?

Comment thread packages/cli/src/lib/wasm/prepare.ts
Comment thread packages/cli/src/commands/debug-files/prepare.ts
Comment thread apps/cli-docs/src/fragments/commands/debug-files.md Outdated
Comment thread apps/cli-docs/src/fragments/commands/debug-files.md Outdated
Comment thread packages/cli/src/commands/debug-files/read-file.ts
Comment thread packages/cli/src/commands/debug-files/prepare.ts Outdated
Comment thread packages/cli/src/commands/debug-files/prepare.ts Outdated
Comment thread packages/cli/src/commands/debug-files/prepare.ts
isWasmPath(path) &&
!isDebugCompanionPath(path) &&
!isIgnored(path, ignoreMatchers, paths)
);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Not fully familiar with the logic but why can we not build an in-memory tree as we do the first scan and use that later on?

Comment thread packages/cli/src/lib/formatters/wasm-prepare.ts Outdated
Comment thread packages/cli/src/lib/wasm/binary.ts

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

FYI did not review this file yet.

- reuse shared readSourceFile in bundle-sources instead of an inline
  readFileSync try/catch
- build the prepare report by pushing module sections into the summary
  array instead of spread/flatMap intermediates
- name all thirteen non-custom wasm section ids as constants in
  SECTION_ORDER
- trim the prepare docs fragment: drop the wasm-split parity note and
  shorten the idempotency line
- Move buildIgnoreMatcher into lib/scan/ignore.ts
- Reuse it in debug-files prepare and sourcemap commands

@loewenheim loewenheim left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

On a high level I think this command is misnamed, on two counts:

  1. It doesn't apply to debug files in general, only wasm.
  2. "Prepare" doesn't sound like it uploads files, it sounds like something you call before you upload them.

I'm just spitballing, but I could see this as a --split-wasm flag on debug-files upload that splits wasm files with default options (and you can either add options to customize the wasm-split behavior or tell users to run wasm-split manually if they need to customize it).

@d2anamaria

Copy link
Copy Markdown
Contributor Author

I'm just spitballing, but I could see this as a --split-wasm flag on debug-files upload that splits wasm files with default options (and you can either add options to customize the wasm-split behavior or tell users to run wasm-split manually if they need to customize it).

I agree the name is a bit misleading, but I'd fix the name rather than folding it into upload. This command rewrites your build artifacts, while upload is read-only. Carrying the wasm-split flags over would bloat upload with options that do nothing unless you're uploading wasm.

@loewenheim

Copy link
Copy Markdown

Yeah, that's fair both on the read-only point and the options point. Again, just a thought: maybe the new command could be folded into wasm-split.

- name companions <stem>.<build_id>.debug.wasm instead of <stem>.debug.wasm
- resolve build id before naming so unstamped modules get unique filenames
- write external_debug_info as a path relative to the module, not basename only
- dry-run reports exact path when id is known, <build-id> placeholder otherwise
- update prepare help text and docs to match the new companion naming
Comment on lines +394 to +402
if (
buildIdsMatch(
await readCompanionBuildId(expectedCompanion),
inspection.buildId
)
) {
return { companion: expectedCompanion, quality: inspection.quality };
}
return null;

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: findExistingCompanion returns the main file's quality, not the companion's. This bypasses the --require-dwarf check, causing a DWARF-less companion file to be uploaded.
Severity: MEDIUM

Suggested Fix

The findExistingCompanion function should inspect the companion file and return its actual debug quality, rather than propagating the quality from the main WASM file's inspection result. This ensures that the subsequent --require-dwarf check correctly identifies if the companion lacks necessary debug information.

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#L394-L402

Potential issue: When preparing a WASM file that has no DWARF info but has a `build_id`
and an existing companion file (which also lacks DWARF), the `--require-dwarf` flag is
incorrectly ignored. The `findExistingCompanion` function mistakenly returns the quality
of the main file ("none") instead of inspecting the companion file's quality. This leads
to the result being marked as `"already-prepared"` with `quality: "none"`. The
subsequent `lacksDwarf` check then incorrectly passes because the action is not
`"skipped"`, allowing a DWARF-less companion file to be uploaded, which will cause
symbolication to fail silently.

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.

3 participants