Skip to content

Commit 1b9bb95

Browse files
authored
feat: external sourcemap upload for compiled binaries (#518)
## Problem Compiled Bun binaries produce minified stack traces with names like `BJ8`, `pp1`, `kQ8` — making Sentry issue grouping inaccurate: - **False splits**: CLI-1D, CLI-BW, CLI-98 are the *same* `SeerError` but split into 3 issues (237 users) because different binary versions produce different minified names - **False merges**: CLI-N groups 84 users worth of different errors (`Internal Error`, `You do not have permission`) into one issue because they share the same minified function name - **Lost context**: Stack traces show `func(bin)` instead of `handleResolvedTargets(issue/list.ts)` ## Solution: Two-Step Build with External Sourcemap Upload ``` Step 1: Bun.build() → bin.js + bin.js.map (bundle TS, no compile) sentry-cli → inject debug IDs (into JS + map) sentry-cli → upload to Sentry (with /$bunfs/root/ prefix) Step 2: Bun.build() → sentry-linux-x64 (compile JS, no sourcemap) ``` The sourcemap is uploaded to Sentry for server-side resolution — never embedded in or shipped with the binary. ## Verified End-to-End ✅ Triggered a test error with the built binary. Sentry now shows **fully resolved stack traces**: **Before** (minified): ``` at G8 (/$bunfs/root/bin.js:23947:19) [in-app] ``` **After** (resolved with source context): ``` at throwApiError (../src/lib/api/infrastructure.ts:48:9) 43 | const status = response?.status ?? 0; 44 | const detail = 45 | error && typeof error === "object" && "detail" in error 46 | ? stringifyUnknown((error as { detail: unknown }).detail) 47 | : stringifyUnknown(error); > 48 | throw new ApiError( 49 | `${context}: ${status} ${response?.statusText ?? "Unknown"}`, at unwrapResult (../src/lib/api/infrastructure.ts:88:5) ``` ## Measured Impact (linux-x64) ``` ╔══════════════════════════════╤══════════════╤════════════════════╗ ║ Metric │ No Sourcemap │ External Upload ║ ║ │ (current) │ (this PR) ║ ╠══════════════════════════════╪══════════════╪════════════════════╣ ║ Gzipped download │ 29.32 MB │ 29.36 MB ║ ║ Δ vs current │ — │ +0.04 MB (+0.1%) ║ ╟──────────────────────────────┼──────────────┼────────────────────╢ ║ bsdiff+zstd (V1→V2) │ 17.43 KB │ 18.26 KB ║ ║ Δ vs current │ — │ +0.83 KB ║ ╟──────────────────────────────┼──────────────┼────────────────────╢ ║ Raw binary │ 101.81 MB │ 102.34 MB ║ ║ Δ vs current │ — │ +0.54 MB (+0.5%) ║ ╟──────────────────────────────┼──────────────┼────────────────────╢ ║ SM file (Sentry only) │ — │ 7.93 MB ║ ╚══════════════════════════════╧══════════════╧════════════════════╝ ``` For comparison, inline sourcemaps would cost +2.30 MB gzipped and +37 KB per delta — **~60× worse** on download size. ## Key Implementation Details - **`sentry-cli sourcemaps inject`** adds debug IDs to the JS file before compile. This provides belt-and-suspenders matching: debug ID (primary) + filename with `/$bunfs/root/` prefix (fallback) - **`--url-prefix '/$bunfs/root/'`** matches the virtual filesystem path Bun uses in compiled binary stack traces - **`minify: false` in Step 2** because the JS is already minified in Step 1 — avoids double-minification producing different output - Upload is **non-fatal**: local builds and PR checks without `SENTRY_AUTH_TOKEN` still work, just skip upload ## Changes ### `script/build.ts` - **Step 1**: `Bun.build()` with `sourcemap: "external"` and `minify: true` → `bin.js` + `bin.js.map` - **Sourcemap upload**: `sentry-cli sourcemaps inject` + `upload` with `--url-prefix` (non-fatal) - **Step 2**: `Bun.build()` with `compile` and `minify: false` → native binary - Intermediate files cleaned up after compile ### `.github/workflows/ci.yml` - Pass `SENTRY_AUTH_TOKEN` to the `build-binary` job
1 parent 973e633 commit 1b9bb95

File tree

3 files changed

+178
-59
lines changed

3 files changed

+178
-59
lines changed

.github/workflows/ci.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ jobs:
225225
- name: Build
226226
env:
227227
SENTRY_CLIENT_ID: ${{ vars.SENTRY_CLIENT_ID }}
228+
# Sourcemap upload to Sentry (non-fatal, skipped when token is absent)
229+
SENTRY_AUTH_TOKEN: ${{ secrets.SENTRY_AUTH_TOKEN }}
228230
# Set on main/release branches so build.ts runs binpunch + creates .gz
229231
RELEASE_BUILD: ${{ github.event_name != 'pull_request' && '1' || '' }}
230232
run: bun run build --target ${{ matrix.target }}

0 commit comments

Comments
 (0)