Source Maps
Upload source maps to TracePath so that minified stack traces in production exceptions are deobfuscated back into your original source.
This half is live. The upload endpoint works today and takes plain multipart HTTP, so any build pipeline can use it. There is no TracePath upload CLI to install: the endpoint is the whole interface. The browser SDK that produces the minified traces on the other end is not published yet.
Emit the maps first
Most bundlers do not write .map files for a production build unless you ask them to. Turn
it on before wiring up any upload:
| Bundler | Setting |
|---|---|
| Vite | build: { sourcemap: true } in vite.config.js |
| Rollup | output: { sourcemap: true } |
| webpack | devtool: "source-map" |
| Next.js | productionBrowserSourceMaps: true in next.config.js |
After a build, confirm the files actually exist before you add an upload step to CI:
ls dist/assets/*.mapGet an upload token
Uploads authenticate with a dedicated upload token, not the project token used for telemetry. Generate it in the dashboard: open the Connection page for your project at app.tracepath.dev (opens in a new tab), find the source map upload card, and click Generate Upload Token. The token is per-project and can be copied from the same place later.
Treat it like any other CI secret. Regenerate issues a new token and invalidates the
current one immediately, so any pipeline still using the old token starts failing until its
secret is updated. Members with the readonly role cannot generate or regenerate tokens; ask
an organization admin.
Upload
POST https://app.tracepath.dev/api/sourcemaps/upload
Authorization: Bearer <upload token>
Content-Type: multipart/form-dataFiles go in the repeatable multipart field files. Upload each bundle together with its map:
curl --fail -X POST "https://app.tracepath.dev/api/sourcemaps/upload" \
-H "Authorization: Bearer $TRACEPATH_SOURCEMAP_TOKEN" \
-F "files=@dist/assets/index-DZ8aDdLF.js" \
-F "files=@dist/assets/index-DZ8aDdLF.js.map"The bundle next to the map is what lets the backend resolve the enclosing function name for each frame. Without it, symbolication still resolves file, line and column, but function names stay as the browser reported them.
The multipart filename becomes the stored name, and stack frames match against the
basename of the frame's file, so send plain filenames without directory paths. curl -F
already does this: it uses the basename of the local path.
The contract
| Behavior | Detail |
|---|---|
| Accepted extensions | .map, .js, .cjs, .mjs. Any other extension is silently skipped, not rejected |
| Per-file size limit | 50 MB. A larger file fails the request with 400 and a message naming the file |
| Total request size | 250 MB across all files in one request. A larger body fails with 413 |
| File count | A request with too many parts fails with 422 and a message asking you to split the upload; the parser's cap is 1000 parts, counting bundles and maps together |
| No files | A request with an empty files field fails with 400 |
| Auth failure | A missing or invalid token returns 401 with no body |
| Response | 200 with {"uploaded": N}, counting only the stored files — skipped files are excluded |
| Debug IDs | Detected server-side in the uploaded content; no extra fields or flags needed |
| Effect | Immediate. Cached resolvers for the uploaded filenames are evicted before the response, and precompiled .tw resolvers are rebuilt in the background so the first exception after a deploy finds a warm cache |
A 200 with {"uploaded": 0} means every file you sent was skipped for its extension. That
is the failure mode worth alerting on, because the request itself succeeded.
CI example (GitHub Actions)
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 22
- run: npm ci
- run: npm run build
- name: Upload source maps
env:
TRACEPATH_SOURCEMAP_TOKEN: ${{ secrets.TRACEPATH_SOURCEMAP_TOKEN }}
run: |
set -eu
args=""
for f in dist/assets/*.map dist/assets/*.js; do
if [ -e "$f" ]; then
args="$args -F files=@$f"
fi
done
if [ -z "$args" ]; then
echo "no build output to upload" >&2
exit 1
fi
# shellcheck disable=SC2086
curl --fail -X POST "https://app.tracepath.dev/api/sourcemaps/upload" \
-H "Authorization: Bearer $TRACEPATH_SOURCEMAP_TOKEN" \
$argsThe emptiness check is deliberate: without it, a build that emitted no maps uploads nothing and the step still goes green.
Maps without source file names
Some build pipelines produce source maps whose sources array contains null entries: the
mappings and embedded source content are intact, but the original file names are missing.
TracePath still deobfuscates those frames, resolving line, column and function name, and shows
<unknown> in place of the file name (for example <unknown>:3:9). To get real file names,
configure your bundler to emit sources entries.
How maps are matched
There is nothing to configure at runtime. When an exception is captured, TracePath resolves each minified frame in two steps:
- By debug ID, when the exception carries one for the frame's file. Every bundle and its map share an embedded unique ID, and the backend picks the map uploaded with that exact ID — immune to filename collisions and concurrent deploys. See Debug IDs.
- By filename otherwise: the map uploaded under the frame's filename is used, most recent upload wins. Content-hashed bundle names, the default in Vite, Next and most bundlers, keep every build's maps distinct automatically.
Uploads take effect immediately, so exceptions arriving right after a deploy resolve against the new maps. See JavaScript symbolication for the full resolution pipeline.
An SDK's version option is unrelated to source maps: it is metadata shown on exceptions for
filtering by build, and can be set or omitted independently of uploads.
Next steps
- Debug IDs: exact build-to-map matching
- Symbolication: how a frame resolves