Build Pipeline
Source Maps
Make production stack traces readable. One line in your build script + one env var in your CI. Five-minute setup once, then forget about it.
TL;DR — the whole integration
- Generate a token in Frontend Project → Settings → API Tokens.
- Add
RELIABLE_TOKENas a secret in your CI / Vercel / Railway settings. - Append
&& npx --yes @reliableapp/frontend-cli sourcemaps upload --dist=./your-build-folder --url-prefix=https://your-domain/to your build script. (Or install as a devDep once and drop thenpx --yes.) - Pass
releasetoinit()in your SDK setup.
Why bother#
Production JavaScript is minified. Your calculateUserDiscount() becomes c(), your file structure collapses into a single line of main.abc.js, and a stack trace ends up looking like:
TypeError: c is not a function
at d (https://app.example.com/main.abc.js:1:42839)
at https://app.example.com/main.abc.js:1:67120Useless to a human. Source maps are JSON files (.js.map) your bundler emits next to each minified bundle. They contain a lookup table — "line 1, column 42839 of main.abc.js is actually line 47 of src/checkout/Calculator.ts, in function calculateUserDiscount" — plus the original source code embedded as strings.
Upload your .js.map files to Reliable from your CI / build pipeline, and the dashboard will resolve every minified frame back to the real one. Skip this and stack traces stay minified, but everything else (replay, breadcrumbs, network calls, browser state) still works.
Most teams don't need this on day one
How it works end-to-end#
Three pieces have to line up:
- SDK tags every event with a
releaseidentifier (a commit SHA, version tag, build ID — anything unique per deploy). - CLI uploads each
.js.mapfrom your build to the Reliable backend, keyed by the same release ID. - Backend, when the dashboard requests an error's resolved stack, looks up the maps for that release and translates each frame.
The release ID is the only thing connecting the three. If your SDK sends release: "abc123" but the CLI uploaded under def456, frames stay minified. Match them or nothing resolves.
Setup#
1. Pass release to init()
Add the release option when you initialize the SDK. Most teams bind it to the commit SHA at build time:
import { init } from '@reliableapp/react';
init({
publicKey: 'pk_live_...',
release: process.env.NEXT_PUBLIC_GIT_SHA, // or whatever build var your bundler exposes
});The trick is getting a value into process.env.NEXT_PUBLIC_GIT_SHA at build time — browser code can't read shell variables at runtime. Each bundler exposes a different convention:
| Name | Type | Default | Description |
|---|---|---|---|
| Next.js | NEXT_PUBLIC_* | — | Any env var prefixed with NEXT_PUBLIC_ is automatically inlined into client bundles. Set it in CI: NEXT_PUBLIC_GIT_SHA=$GITHUB_SHA npm run build (Vercel auto-injects NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA — use that one directly there). |
| Vite | VITE_* via import.meta.env | — | Set VITE_GIT_SHA in your CI environment. Read it in code as import.meta.env.VITE_GIT_SHA. Auto-inlined at build time, no plugin needed. |
| Create React App | REACT_APP_* | — | Set REACT_APP_GIT_SHA in CI. Read in code as process.env.REACT_APP_GIT_SHA. Auto-inlined at build time. |
| Webpack | DefinePlugin | — | new webpack.DefinePlugin({ 'process.env.GIT_SHA': JSON.stringify(process.env.GIT_SHA) }) — replaces the literal string at build time. |
| Astro / Nuxt / SvelteKit | PUBLIC_* / VITE_* / etc. | — | All Vite-based — same pattern as Vite above (each enforces its own prefix; check the framework docs). |
The CI side matches automatically
GITHUB_SHA, VERCEL_GIT_COMMIT_SHA, etc., directly without the bundler-prefix dance. Just make sure both ends point at the same source — usually $GITHUB_SHA in GitHub Actions, $VERCEL_GIT_COMMIT_SHA on Vercel, etc. — and they'll always match.Universal: read the SHA inside your bundler config
On Vercel/Netlify/GitHub Actions a SHA env var is auto-injected. On Dokploy/Coolify/Nixpacks/self-hosted CIs there's no auto-set value — but if .git/ is in your build context (or you removed it from .dockerignore), you can derive the SHA inside your bundler config. This works everywhere:
Next.js — next.config.js:
const { execSync } = require('child_process');
function gitSha() {
return process.env.NEXT_PUBLIC_VERCEL_GIT_COMMIT_SHA // Vercel
|| process.env.NEXT_PUBLIC_GIT_SHA // user-set on any platform
|| process.env.RAILWAY_GIT_COMMIT_SHA // Railway
|| process.env.RENDER_GIT_COMMIT // Render
|| process.env.SOURCE_COMMIT // Coolify / Heroku
|| tryGit();
}
function tryGit() {
try { return execSync('git rev-parse HEAD').toString().trim(); }
catch { return null; }
}
module.exports = {
productionBrowserSourceMaps: true,
env: { NEXT_PUBLIC_GIT_SHA: gitSha() },
};Vite — vite.config.ts:
import { defineConfig } from 'vite';
import { execSync } from 'child_process';
function gitSha(): string {
if (process.env.VITE_GIT_SHA) return process.env.VITE_GIT_SHA;
if (process.env.VERCEL_GIT_COMMIT_SHA) return process.env.VERCEL_GIT_COMMIT_SHA;
if (process.env.RAILWAY_GIT_COMMIT_SHA) return process.env.RAILWAY_GIT_COMMIT_SHA;
if (process.env.SOURCE_COMMIT) return process.env.SOURCE_COMMIT;
try { return execSync('git rev-parse HEAD').toString().trim(); }
catch { return ''; }
}
export default defineConfig({
build: { sourcemap: true },
define: {
'import.meta.env.VITE_GIT_SHA': JSON.stringify(gitSha()),
},
});Dokploy / Coolify / Docker-based PaaS gotcha
.git/ from the Docker build context, so git rev-parse HEAD fails inside the build. Add !.git to your .dockerignore so the bundler config can read it:# .dockerignore
node_modules
.next
dist
# explicitly include .git so build-time scripts can read the commit SHA
!.git.git/ is in the context, both the SDK's release tag (read by your bundler config) AND the CLI's --release auto-detect work without any further configuration.2. Generate an API token
In the dashboard, go to Frontend Project → Settings → API Tokens and click Create token. Give it a descriptive name (e.g. "GitHub Actions", "Vercel CI"). Copy the rl_fpt_... value immediately — it is shown only once.
Tokens are scoped per frontend project
3. Install + wire the CLI into your build
The CLI is published as @reliableapp/frontend-cli. For a package.json build script, install it as a devDependency once so the binary resolves on every run:
npm install --save-dev @reliableapp/frontend-cliNow reliableapp-frontend is available inside any npm script. For one-off CI YAML steps, you can skip the install and use npx --yes @reliableapp/frontend-cli instead — both work, install is just faster on repeat builds.
Why your build script may say "not recognized"
reliableapp-frontend only resolves on PATH if (a) you installed the package as a dep, or (b) you wrap the call with npx --yes. If you run the script outside npm (e.g. directly in your shell) without a global install, you'll get a "command not found" error.Pattern A: explicit CI step (GitHub Actions, GitLab, CircleCI…)#
If your platform lets you write build steps directly, add an upload step after your build:
# .github/workflows/deploy.yml
- name: Build
run: npm run build
- name: Upload sourcemaps
env:
RELIABLE_TOKEN: ${{ secrets.RELIABLE_TOKEN }}
run: |
npx @reliableapp/frontend-cli sourcemaps upload \
--dist=./dist \
--url-prefix=https://app.example.com/Just RELIABLE_TOKEN — the token already names the frontend project, so there's nothing else to wire up.
Pattern B: package.json build script (Vercel, Netlify, Railway, Render, Coolify, Dokploy…)#
PaaS platforms that auto-build on push don't expose a step-injection point — they just run npm run build. Install the CLI as a devDependency, then chain it into the build script:
npm install --save-dev @reliableapp/frontend-cli{
"scripts": {
"build": "vite build && reliableapp-frontend sourcemaps upload --dist=./dist --url-prefix=https://app.example.com/"
}
}Or skip the install and use npx --yes instead (slower on repeat builds because the package is downloaded each time):
{
"scripts": {
"build": "vite build && npx --yes @reliableapp/frontend-cli sourcemaps upload --dist=./dist --url-prefix=https://app.example.com/"
}
}Set RELIABLE_TOKEN in the platform's environment variables UI (every PaaS has a secrets section). The CLI reads it automatically and auto-detects the commit SHA from the platform's own variable, so nothing else is needed.
Why no --release flag here?
VERCEL_GIT_COMMIT_SHA, RAILWAY_GIT_COMMIT_SHA, RENDER_GIT_COMMIT, COMMIT_REF, etc). When omitted, --release resolves to whichever one your platform sets. Pass it explicitly only if you're on a custom system.How --dist and --url-prefix work#
The CLI walks --dist for every *.js.map file, then computes the corresponding browser-visible JS URL by:
url-prefix + path-relative-to-dist (with .map stripped)Always run --dry-run first to print the computed URLs and verify they match what the browser actually fetches.
Per-framework setup#
Most frameworks don't emit production sourcemaps by default — opt in first, then point the CLI at the right output folder. Below are tested recipes for the major bundlers.
The recipes use npx — install for speed
npx --yes @reliableapp/frontend-cli so it works standalone without any prior install. For repeat builds (every CI run, every deploy) you'll want to install the package once with npm install --save-dev @reliableapp/frontend-cli and then drop the npx --yes prefix — the bare reliableapp-frontend binary resolves automatically inside any npm script.Vite
// vite.config.ts
export default defineConfig({
build: {
sourcemap: true,
},
});npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./dist \
--url-prefix=https://app.example.com/Vite emits maps under dist/assets/. The CLI walks recursively, so --dist=./dist picks them all up.
Next.js
// next.config.js
module.exports = {
productionBrowserSourceMaps: true,
};npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./.next/static \
--url-prefix=https://app.example.com/_next/static/Use --dist=./.next/static, not ./.next
.next/. The browser only loads from .next/static/* served at /_next/static/. Pointing at the parent folder uploads useless server-side maps and can make resolution match the wrong file.Create React App
CRA emits sourcemaps by default unless you set GENERATE_SOURCEMAP=false.
npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./build/static/js \
--url-prefix=https://app.example.com/static/js/Remix (v2+)
npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./build/client \
--url-prefix=https://app.example.com/Remix v2+ uses Vite under the hood and emits maps in build/client/assets/ by default when build.sourcemap is on in vite.config.ts.
SvelteKit
// vite.config.ts (SvelteKit uses Vite internally)
export default defineConfig({
build: { sourcemap: true },
});npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./.svelte-kit/output/client \
--url-prefix=https://app.example.com/Astro
// astro.config.mjs
export default defineConfig({
vite: { build: { sourcemap: true } },
});npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./dist \
--url-prefix=https://app.example.com/Nuxt 3
// nuxt.config.ts
export default defineNuxtConfig({
sourcemap: { client: true },
});npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./.output/public/_nuxt \
--url-prefix=https://app.example.com/_nuxt/Plain Webpack
// webpack.config.js
module.exports = {
devtool: 'source-map', // production-quality maps; do NOT use 'eval-source-map' in prod
};npx --yes @reliableapp/frontend-cli sourcemaps upload \
--dist=./dist \
--url-prefix=https://app.example.com/Anything else
The pattern is always the same — find the folder where your bundler writes *.js.map files, point --dist at it, and set --url-prefix to the URL your CDN serves that folder from. If you can curl https://your-cdn.com/path/main.abc.js in the browser, the asset URL the CLI computes for ./your-build-output/main.abc.js.map should be exactly that.
Run --dry-run before --force or in CI
All CLI options#
| Name | Type | Default | Description |
|---|---|---|---|
| --token <token> | $RELIABLE_TOKEN | — | API token (rl_fpt_...). The token already names the frontend project — no other IDs needed. |
| --release <id> | auto-detect | — | Release ID. Sniffed from common platform env vars when omitted. |
| --dist <path> | required | — | Local path to the build output folder. |
| --url-prefix <url> | required | — | Browser-visible URL prefix that maps to the dist root. |
| --environment <env> | production | — | production | staging | development. |
| --api <url> | Reliable backend | — | Override for self-hosted backends. |
| --concurrency <n> | 4 | — | How many uploads to run in parallel. |
| --force | off | — | Bypass the CI safety check (refuses to run on a developer laptop by default). |
| --dry-run | off | — | Walk the dist folder and print computed URLs; don't actually upload. |
Auto-detected commit SHA#
When --release is omitted, the CLI checks the following env vars in order. The first non-empty one wins:
GITHUB_SHA # GitHub Actions
CI_COMMIT_SHA # GitLab CI
VERCEL_GIT_COMMIT_SHA # Vercel
COMMIT_REF # Netlify
RAILWAY_GIT_COMMIT_SHA # Railway
RENDER_GIT_COMMIT # Render
CF_PAGES_COMMIT_SHA # Cloudflare Pages
SOURCE_COMMIT # Coolify, Heroku
CIRCLE_SHA1 # CircleCI
BUILDKITE_COMMIT # Buildkite
BITBUCKET_COMMIT # Bitbucket Pipelines
BUILD_SOURCEVERSION # Azure Pipelines
GIT_COMMIT # Jenkins / generic
COMMIT_SHA # genericPrivacy#
Source map files contain your original source code. The bundler embeds it inside the .js.map as JSON strings — that's how the resolver shows you readable code, and it's how every error tracker on the market works.
Don't deploy .js.map files publicly
.js.map file alongside .js, anyone can download it and reconstruct your frontend source. The standard pattern: emit maps in your build, upload them privately to Reliable via this CLI, then strip them from your deployed bundle (or never deploy them at all).If your team can't ship source code to a third party, configure your bundler to emit maps without sourcesContent (Webpack: devtool: 'nosources-source-map'; Vite: a custom plugin that strips the field). You'll see real function names, file paths, and line numbers in stack traces, but no code preview snippets.
Local-build safety#
The CLI refuses to upload when no CI environment variable is detected — running npm run build on your laptop will not pollute the release index with throwaway local builds. Pass --force only when you genuinely intend to upload from a non-CI context (for example, manually re-uploading a missed release).
Troubleshooting#
401 Unauthorized
The token is invalid or has been revoked. Confirm RELIABLE_TOKEN is set in your CI secrets and starts with rl_fpt_. Generate a fresh token if you copied an old one or aren't sure.
403 Token missing required scope
The token doesn't have the sourcemaps:write scope. Newly created tokens get it by default — if you're seeing this, the token may be from a different system or scoped differently. Generate a fresh one from the dashboard.
"No .js.map files found"
Your bundler isn't emitting source maps. Most defaults skip them in production builds:
- Vite:
build.sourcemap: trueinvite.config.ts. - Webpack:
devtool: 'source-map'for production. - Next.js:
productionBrowserSourceMaps: trueinnext.config.js.
Stacks still showing minified after upload
Most likely cause: the release in the SDK config doesn't match the release the CLI uploaded under. Both must be exactly the same string. Confirm by:
- Inspecting an error event in the dashboard — the
releasefield should equal the value the CLI logged. - Running the CLI with
--dry-runto see which release would be used. - Verifying the
--url-prefixproduces URLs that exactly match the file URLs in your stack traces (run with--dry-runto print them).