Skip to content

feat: native debug ID injection and sourcemap upload#543

Merged
BYK merged 10 commits intomainfrom
feat/native-debug-id-injection
Mar 24, 2026
Merged

feat: native debug ID injection and sourcemap upload#543
BYK merged 10 commits intomainfrom
feat/native-debug-id-injection

Conversation

@BYK
Copy link
Member

@BYK BYK commented Mar 23, 2026

Summary

Replace npx @sentry/cli sourcemaps inject and @sentry/esbuild-plugin with native TypeScript implementations. Zero new dependencies — only uses node:crypto, node:fs/promises, node:zlib, and existing p-limit.

What changed

Debug ID Injection (script/debug-id.ts)

  • contentToDebugId(): SHA-256 → UUID v4 (byte-for-byte compatible with @sentry/bundler-plugin-core's stringToUUID, verified against the actual package)
  • getDebugIdSnippet(): Runtime IIFE that registers debug IDs in globalThis._sentryDebugIds
  • injectDebugId(): Injects snippet + comment into JS, adjusts sourcemap mappings, adds debugId/debug_id fields
  • Idempotent, hashbang-safe, always runs (even without SENTRY_AUTH_TOKEN)

Streaming ZIP Builder (src/lib/sourcemap/zip.ts)

  • Writes entries to disk via node:fs/promises FileHandle — only one compressed entry in memory at a time
  • Uses raw DEFLATE (node:zlib) for compression, STORE for empty files
  • Produces valid ZIP archives extractable by standard tools

Sourcemap Upload API (src/lib/api/sourcemaps.ts)

  • Implements Sentry's chunk-upload + assemble protocol natively
  • Parallel chunk uploads via p-limit with server-configured concurrency
  • Gzip compression for chunks (server only supports gzip — verified on US and DE regions)
  • Streaming: reads ZIP back in 8MB chunks for upload, never holds full bundle in memory

Build Pipeline

  • script/build.ts: Uses native injection + upload (removed execSync('npx @sentry/cli ...'))
  • script/bundle.ts: Custom sentrySourcemapPlugin esbuild plugin replaces @sentry/esbuild-plugin

Dependency Removal

  • @sentry/esbuild-plugin removed from devDependencies
  • Its transitive @sentry/cli (Rust binary) dependency is eliminated

Tests

  • 13 tests for debug-id (5 property-based, 8 unit)
  • 10 tests for ZipWriter (2 property-based, 5 unit, 3 binary format)

Follow-up (PR 3)

User-facing CLI commands (sentry sourcemap inject/upload) with worker-based parallel processing — tracked separately.

BYK added 2 commits March 23, 2026 23:00
Replace `npx @sentry/cli sourcemaps inject` with a pure TypeScript
implementation in script/debug-id.ts. Zero new dependencies — uses
only node:crypto and node:fs/promises.

The implementation:
- contentToDebugId(): SHA-256 content hash → UUID v4 format,
  byte-for-byte compatible with @sentry/bundler-plugin-core's
  stringToUUID (verified against the actual package)
- getDebugIdSnippet(): Runtime IIFE that registers debug IDs in
  globalThis._sentryDebugIds, matching the bundler plugin's snippet
- injectDebugId(): Injects snippet + comment into JS, adjusts
  sourcemap mappings, adds debugId/debug_id fields to sourcemap JSON

Key behaviors:
- Idempotent: files with existing //# debugId= are skipped
- Hashbang-safe: preserves #!/usr/bin/env lines at top of file
- Sourcemap-aware: prepends one ; to mappings for line offset
- Always injects debug IDs (even without SENTRY_AUTH_TOKEN) so local
  builds get debug IDs for development/testing

Tests: 13 tests (5 property-based via fast-check, 8 unit tests)
covering UUID format, determinism, idempotency, hashbang preservation,
and sourcemap mutation.
Replace `npx @sentry/cli sourcemaps upload` and `@sentry/esbuild-plugin`
with a native TypeScript implementation. Zero new dependencies.

New modules:
- src/lib/sourcemap/zip.ts: Streaming ZIP builder using node:fs/promises
  FileHandle + node:zlib deflateRaw. Writes entries to disk one at a time
  — memory usage is O(one compressed entry + central directory metadata).
  Uses STORE method for empty files, DEFLATE for everything else.
- src/lib/api/sourcemaps.ts: Implements the Sentry chunk-upload + assemble
  protocol (6-step flow: get options → build artifact bundle → chunk + hash
  → assemble → upload missing chunks in parallel → poll until done).
  Uses gzip compression for chunks (server only supports gzip, tested
  on both US and DE regions). Parallel uploads via p-limit.

Build pipeline changes:
- script/build.ts: Uses uploadSourcemaps() instead of execSync('npx ...')
  for the Bun binary build. Always injects debug IDs even without auth.
- script/bundle.ts: Custom sentrySourcemapPlugin esbuild plugin replaces
  @sentry/esbuild-plugin. Uses onEnd hook → injectDebugId + uploadSourcemaps.

Dependency removal:
- @sentry/esbuild-plugin removed from devDependencies (and its transitive
  @sentry/cli Rust binary dependency is eliminated)

Tests: 10 tests for ZipWriter (5 unit + 2 property-based + 3 binary format)
verifying round-trip integrity, empty file handling, and ZIP structure.
@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

Semver Impact of This PR

🟡 Minor (new features)

📋 Changelog Preview

This is how your changes will appear in the changelog.
Entries from this PR are highlighted with a left border (blockquote style).


New Features ✨

  • Native debug ID injection and sourcemap upload by BYK in #543

Internal Changes 🔧

  • (ci) Upgrade GitHub Actions to Node 24 runtime by BYK in #542
  • (coverage) Make checks informational on release branches by BYK in #541
  • Regenerate skill files by github-actions[bot] in ec1ffe28

🤖 This preview updates automatically when you update the PR.

@github-actions
Copy link
Contributor

github-actions bot commented Mar 23, 2026

Codecov Results 📊

126 passed | Total: 126 | Pass Rate: 100% | Execution Time: 0ms

📊 Comparison with Base Branch

Metric Change
Total Tests
Passed Tests
Failed Tests
Skipped Tests

✨ No test changes detected

All tests are passing successfully.

✅ Patch coverage is 100.00%. Project has 1028 uncovered lines.
✅ Project coverage is 96.03%. Comparing base (base) to head (head).

Coverage diff
@@            Coverage Diff             @@
##          main       #PR       +/-##
==========================================
+ Coverage    96.02%    96.03%    +0.01%
==========================================
  Files          185       186        +1
  Lines        25805     25897       +92
  Branches         0         0         —
==========================================
+ Hits         24777     24869       +92
- Misses        1028      1028         —
- Partials         0         0         —

Generated by Codecov Action

- Add try/finally in uploadSourcemaps() to always clean up the temp
  ZIP file, even on error. Extracted uploadArtifactBundle() as inner
  function to keep the cleanup boundary clean.
- Merge overall checksum computation into hashChunks() — maintains a
  running SHA-1 hasher alongside per-chunk hashing, eliminating the
  redundant full-file re-read.
- Use STORE method for empty files in ZipWriter (DEFLATE of empty
  input confuses some extractors).
- Use options object for uploadArtifactBundle() to stay under the
  4-parameter lint limit.
Prevents file handle leak if stat(), read(), or hash operations
throw during chunk processing.
- sentrySourcemapPlugin is now always added to the esbuild plugins
  array. The upload step inside it is already gated on
  SENTRY_AUTH_TOKEN, so debug IDs are always injected even for local
  builds without auth — matching the behavior in build.ts.
- Wrap chunk upload file handle in try/finally to prevent leaks if
  read() throws, same pattern as the hashChunks fix.
- ZipWriter.finalize() now wraps central directory writes in
  try/finally to always close the file handle.
- Added ZipWriter.close() for error cleanup when addEntry fails
  partway through (closes handle without finalizing).
- buildArtifactBundle wraps entry writes in try/catch and calls
  zip.close() on failure.
- Moved buildArtifactBundle call inside the try block in
  uploadSourcemaps so temp file is cleaned up even on build error.
The upload uses the CLI's authenticated fetch via getSdkConfig()
instead of a manually-provided token.
…thToken

- uploadInjectedSourcemaps now returns a boolean indicating success.
  .map files are only deleted after a successful upload, preserving
  them on failure so retrying is possible without a full rebuild.
- Remove unused authToken field from UploadOptions.
- Extract urlToBundlePath() helper used by both manifest builder and
  ZIP entry loop, eliminating divergence risk.
- Handle edge case where JS file has a hashbang (#!) but no trailing
  newline — now inserts a newline separator before the snippet.
Check for error state immediately after the first assemble POST
instead of falling through to the chunk upload + poll loop.
Copy link

@cursor cursor bot left a comment

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.

Fix All in Cursor

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

@BYK BYK merged commit 838ccaa into main Mar 24, 2026
22 checks passed
@BYK BYK deleted the feat/native-debug-id-injection branch March 24, 2026 00:56
BYK added a commit that referenced this pull request Mar 24, 2026
…mands (#547)

## Summary

Add user-facing CLI commands for sourcemap management, completing the
native sourcemap toolchain started in #543.

### New Commands

**`sentry sourcemap inject <dir>`**
- Scans a directory recursively for JS files (`.js`/`.mjs`/`.cjs`) and
their companion `.map` files
- Injects Sentry debug IDs into each pair (idempotent — skips files that
already have debug IDs)
- Supports `--ext` for custom extensions, `--dry-run` for preview
- Skips `node_modules` and hidden directories

**`sentry sourcemap upload [org/project/]<dir>`**
- Uploads sourcemaps to Sentry using the chunk-upload + assemble
protocol (from #543)
- Supports `--release` and `--url-prefix` flags
- Auto-detects org/project from DSN/config or accepts explicit
`org/project/` prefix
- Both `--json` output and human-readable output supported

### Code Changes

- Moved debug ID functions from `script/debug-id.ts` →
`src/lib/sourcemap/debug-id.ts` (canonical location)
- `script/debug-id.ts` becomes a thin re-export (build scripts still
import from here)
- Added `src/lib/sourcemap/inject.ts` for directory scanning + injection
- Registered `sourcemap` route in `app.ts` with `sourcemaps` plural
alias
- Generated SKILL.md and reference docs

### Usage Examples
```bash
# Inject debug IDs into build output
sentry sourcemap inject ./dist

# Preview what would be modified
sentry sourcemap inject ./dist --dry-run

# Upload to Sentry
sentry sourcemap upload ./dist
sentry sourcemap upload my-org/my-project/./dist --release 1.0.0
```

Depends on: #543 (merged)

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
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.

1 participant