Errors USP
Source maps
Production JavaScript is minified — every error reads as
main.a1b2c3.js:1:48202. Upload your source maps to Crossdeck and
every stack frame resolves to readable references like
src/checkout/Pay.tsx:114 — handleSubmit.
Prefer to let an AI assistant do the setup instead? AI-assisted setup →
TL;DR
- Install
@cross-deck/clias a dev dependency. - Make sure your bundler emits external source maps with
sourcesContentinlined. - After every production build, run
npx @cross-deck/cli upload-sourcemaps --release "$GIT_SHA" --url-prefix <your-bundle-url-prefix> ./dist. - Stamp the same release identifier on every Crossdeck event by setting
appVersion: process.env.<FRAMEWORK_PREFIX>_GIT_SHAin your SDK init. - Trigger a test error and confirm the dashboard renders readable file paths.
What is this?
Your bundler — Vite, Webpack, Next.js, Rollup, ESBuild, Parcel — collapses your TypeScript / JSX into a
handful of minified .js files for production. Variable names become a,
functions become o, file paths become main.<hash>.js. Browsers run this
squashed code fine, but when something throws, the stack trace is unreadable.
A source map is the dictionary that translates squashed back into English. Modern bundlers produce one as part of the build. Crossdeck's CLI ships your maps into private cloud storage so our backend can use them when errors arrive, never serving the raw map to clients.
Prerequisites
- A Crossdeck project with at least one app registered (web).
- A Crossdeck secret key (
cd_sk_live_…). Grab one from Developers → API keys. Publishable keys (cd_pub_…) cannot upload sourcemaps — they're client-only. - Node.js 18 or later in your build environment.
- A bundler that emits external
.js.mapfiles withsourcesContentinlined.
Install the CLI
npm install --save-dev @cross-deck/cli
Or pnpm / yarn / bun — same package name, same surface.
Configure your bundler
Source maps need to be emitted as external .js.map files alongside each
.js bundle, with sourcesContent embedded so we have the original source to display.
Vite / Rollup
// vite.config.ts
export default defineConfig({
build: { sourcemap: true },
});
Webpack
// webpack.config.js
module.exports = {
devtool: "source-map",
};
Avoid eval-source-map and inline-source-map for production — those embed the
map in the bundle and can't be uploaded separately.
Next.js
// next.config.js
module.exports = {
productionBrowserSourceMaps: true,
};
ESBuild
// esbuild.config.js
await esbuild.build({
entryPoints: ["src/index.ts"],
bundle: true,
outdir: "dist",
sourcemap: true,
sourcesContent: true,
});
Upload from CI
Run this command from your build pipeline after the production build finishes:
npx @cross-deck/cli upload-sourcemaps \
--release "$GIT_SHA" \
--url-prefix "https://your-app.com/static/js/" \
./dist
The CLI walks ./dist recursively, pairs every .js with its companion
.map, and uploads in batches of ≤100 files per request. The
--url-prefix is the URL your bundles are served from at runtime — without the bundle filename
itself. Examples:
- Vercel + Next.js: usually
https://your-app.vercel.app/_next/. Source map output dir is.next/static. - Vite + Netlify: usually
https://your-site.netlify.app/assets/. Source map output dir isdist/assets. - Custom domain on Firebase Hosting:
https://your-domain.com/(path is just the hosting public dir fromfirebase.json).
Setting CROSSDECK_AUTH_TOKEN
The CLI authenticates with your project's secret key. Store it in your CI secrets, never commit it:
- GitHub Actions: repo settings → Secrets and variables → Actions → new secret.
Then reference as
${{ secrets.CROSSDECK_AUTH_TOKEN }}. - Vercel: project settings → Environment Variables → mark as Sensitive.
- Netlify: site settings → Environment variables → new secret.
- CircleCI / GitLab CI: add as a masked environment variable.
Example: GitHub Actions
name: Deploy
on: [push]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20 }
- run: npm ci
- run: npm run build
- name: Upload source maps to Crossdeck
env:
CROSSDECK_AUTH_TOKEN: ${{ secrets.CROSSDECK_AUTH_TOKEN }}
run: |
npx @cross-deck/cli upload-sourcemaps \
--release "$GITHUB_SHA" \
--url-prefix "https://your-app.com/static/js/" \
./dist
- name: Strip maps from deploy artefact
run: find dist -name '*.map' -delete
Strip the maps from production
Source maps should NEVER be deployed publicly — they leak your original source. After upload to
Crossdeck, delete them from the deploy artefact. The find command in the example above does
this. Alternatively, use your deploy target's exclude config (.vercelignore, etc.).
Verify it worked
- Deploy your app with the upload step in your pipeline.
- Trigger a test error from the deployed app:
throw new Error("crossdeck sourcemap test"); - Open Errors → Issues in the dashboard. The most-recent issue should
show a top frame like
src/<file>.tsx:<line> — <function>instead of the minified hash.
Framework recipes
Next.js (App Router) on Vercel
// next.config.js
module.exports = {
productionBrowserSourceMaps: true,
};
// app/layout.tsx
import { Crossdeck } from "@cross-deck/web";
Crossdeck.init({
appId: process.env.NEXT_PUBLIC_CROSSDECK_APP_ID,
publicKey: process.env.NEXT_PUBLIC_CROSSDECK_KEY,
environment: "production",
appVersion: process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA,
});
// .github/workflows/deploy.yml — after `vercel build`:
npx @cross-deck/cli upload-sourcemaps \
--release "$VERCEL_GIT_COMMIT_SHA" \
--url-prefix "https://$VERCEL_URL/_next/" \
./.next/static
Vite + Netlify
// vite.config.ts
export default defineConfig({
build: { sourcemap: true },
define: { __APP_SHA__: JSON.stringify(process.env.COMMIT_REF) },
});
// src/main.ts
Crossdeck.init({
appId, publicKey, environment: "production",
appVersion: __APP_SHA__,
});
// netlify.toml — postbuild hook:
[build]
command = "npm run build && npx @cross-deck/cli upload-sourcemaps --release $COMMIT_REF --url-prefix https://$URL/assets/ ./dist && find dist -name '*.map' -delete"
Webpack + custom server
// webpack.config.js
module.exports = {
devtool: "source-map",
};
// In your CI (postbuild):
npx @cross-deck/cli upload-sourcemaps \
--release "$CI_COMMIT_SHA" \
--url-prefix "https://your-domain.com/static/js/" \
./dist/static/js
find ./dist -name '*.map' -delete
Troubleshooting
"Source maps not uploaded for this release"
The dashboard shows this card when no sourcemap covers any frame's bundle URL for the event's release. Most common causes:
- The
--url-prefixdoesn't match the runtime URL of your bundles. Compare thefilenamefield on a real error event in the dashboard against the prefix you passed. - The
--releasevalue passed to the CLI doesn't match theappVersionthe SDK stamps on events. Both should be the git SHA (or whatever stable release identifier you use). - The CLI ran but no
.mapfiles were discovered — checknpx @cross-deck/cli upload-sourcemaps --verbose …for the file list it scanned.
"Source map is missing sourcesContent"
Your bundler emitted a map without inline source content. The CLI rejects these because Crossdeck can't display the original source without fetching the raw files at decode time, which is too slow for the dashboard. Fix:
- Vite / Rollup: set
build.sourcemap: true(NOT"hidden"and not justtrueon the Rollup output options — the Vite higher-level toggle handles it). - Webpack:
devtool: 'source-map'(NOT'nosources-source-map'). - ESBuild:
sourcesContent: true.
Stack frames resolve for SOME events but not others
The customer's app probably has multiple bundles (your code + vendor chunks), and you've only uploaded
maps for some. The CLI uploads every .js + .map pair it finds, so re-running with
--verbose against the full dist/ directory should pick up the missing ones.
"Authentication failed"
The CLI accepts secret keys only (cd_sk_…). Publishable keys
(cd_pub_…) are rejected. Get a secret key from
Developers → API keys in the dashboard, store it in CI
secrets as CROSSDECK_AUTH_TOKEN.