Use knotel
Source maps
Turn minified production stack traces back into your own files and lines, with no build plugin and no SDK to maintain.
A production bundle throws from gl (react-dom-Cj2242Ga.js:8:33470), which tells you nothing. Upload the build's source maps and knotel reads the same stack as useProjects (src/hooks/projects.ts:42), with the line of source beside it.
There is no build plugin to install and no SDK to keep up with. A deploy script that can run curl is the whole integration, and the Source maps page in the app takes a drag-and-dropped folder if you would rather not automate it yet.
Emit the maps
Most production builds have source maps switched off. In Vite, turn them on as hidden:
export default defineConfig({
build: { sourcemap: "hidden" },
});hidden writes the .map files but leaves out the //# sourceMappingURL comment that points browsers at them. You upload them to knotel and delete them before the build is published, so they never reach the public origin at all.
hidden only removes the pointer, not the file - if you deploy the maps alongside the bundle, anyone who guesses the URL can read your code. Upload them, then rm them.Upload from CI
Two endpoints: one asks which maps are new, one takes a map. Both accept the project's ingest key in the x-knotel-key header - the same key that sends traces.
cd dist/assets
ls *.js.map | while read m; do
printf '{"file":"%s","digest":"%s"}\n' "${m%.map}" "$(sha256sum "$m" | cut -d' ' -f1)"
done | jq -sc . \
| curl -s -H "x-knotel-key: $KNOTEL_DEPLOY_KEY" --data-binary @- \
https://knotel.example.com/v1/sourcemaps/check \
| jq -r '.missing[]' \
| while read f; do
curl -s -H "x-knotel-key: $KNOTEL_DEPLOY_KEY" --data-binary "@$f.map" \
"https://knotel.example.com/v1/sourcemaps?file=$f"
done
rm *.js.mapkn_ ingest key is public: it ships in the page that sends the telemetry, so anyone who views source has it. The OTLP endpoints tolerate that because a key's allowed sources bound it by Origin and IP - a deploy script has neither. If a public key could upload, any reader of your site could push a map for a real chunk name and choose the file, line and source text your error pages then show.So source maps take a
knd_ deploy key, minted per project on the Source maps page and kept in your build's environment. An ingest key is refused and says why. The page itself needs no key: a signed-in session is already stronger.The first call is what keeps this cheap. A map is stored under the minified filename it explains, and bundlers put a content hash in that name - so a chunk nobody touched keeps its name between deploys and is skipped. A vendor chunk, which is usually the largest map you have, is uploaded exactly once and then never again.
Uploading the same file twice is free and changes nothing.
Or drop a folder
The project's Source maps page has an upload button that takes the whole dist/assets folder. It runs the same check first, so a 50 MB folder usually sends a few megabytes.
The page's first table is the one to look at. It lists the bundles that the last seven days of stack traces actually named, and whether a map is stored for each:
| Bundle | Errors (7d) | Map |
|---|---|---|
| dist-BOmjHgGk.js | 412 | stored |
| react-dom-Cj2242Ga.js | 380 | no map |
| vendor-K2p8xQ1z.js | 31 | no map |
Every deploy renames the chunks, so maps uploaded by hand once stop matching and nothing fails - symbolication just quietly stops. This table is how you notice.
When resolution happens
Stacks are resolved when someone opens the error, not when it arrives. That has one consequence worth knowing:
A frame whose bundle has no map is left as it arrived rather than dropped, so a partly-uploaded project still reads better than nothing. Frames that resolve into node_modules are folded away behind a line you can expand - an error usually throws several frames deep inside a framework, and the first line that is yours is the one you want.
Minified React errors
A production React build replaces its messages with numbers, so what reaches ingest reads Minified React error #185. knotel ships React's own table of those numbers and shows the real sentence - "Maximum update depth exceeded…" - with the code kept underneath. This needs no upload and no build change; it works on errors already stored.
What happens to the maps
Maps live in their own SQLite file (/data/sourcemaps.db) on the volume knotel already mounts. Nothing new to run, and no object storage to configure.
- They are rebuildable. Losing that file costs a re-upload, not data, which is why it is deliberately not part of the pre-upgrade backup of
knotel.db. That backup stays small and fast because the maps are not in it. - They report their own use. The Source maps page shows how many frames each map has resolved and when it was last useful. A map sitting at zero is costing disk for nothing.
- They are evicted, least-recently-used first, once a project passes its budget (200 MB by default,
SOURCEMAP_BUDGET_BYTESto change it). Recently-uploaded maps are never evicted, so a fresh deploy is safe even before anyone has hit an error in it. - You can delete them - one, or all of a project's - from the same page. Space is returned to the filesystem, not just marked free inside the file.
kn_ key is write-only by design - it lives in CI logs and deploy runners - and nothing about pushing maps needs the ability to delete them. Eviction handles the cleanup CI would otherwise want to do.Endpoint reference
| Request | Does |
|---|---|
| POST /v1/sourcemaps/check | Body [{file, digest}]; returns { missing: [...] } |
| POST /v1/sourcemaps?file=<name> | Body is the raw .map file. Upserts; re-uploading the same bytes is a no-op |
| GET /v1/sourcemaps | The project's maps and what they cost |
| DELETE /v1/sourcemaps?file=<name> | Removes one map. Session only; add ?all=1 for all of them |
A key identifies its own project. A signed-in session can name any project with ?project=<id>; a key that disagrees with that parameter is refused rather than quietly writing somewhere else.