Commit 1b9bb95
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` job1 parent 973e633 commit 1b9bb95
3 files changed
+178
-59
lines changed| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
225 | 225 | | |
226 | 226 | | |
227 | 227 | | |
| 228 | + | |
| 229 | + | |
228 | 230 | | |
229 | 231 | | |
230 | 232 | | |
| |||
0 commit comments