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

  1. Generate a token in Frontend Project → Settings → API Tokens.
  2. Add RELIABLE_TOKEN as a secret in your CI / Vercel / Railway settings.
  3. 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 the npx --yes.)
  4. Pass release to init() in your SDK setup.
That's it — works on any framework, any deploy platform. Skip down to Per-framework setup for the exact one-liner for your stack.

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:

text
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:67120

Useless 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

Session replay shows you exactly what the user did, including the line of code that failed visually. If you only have time for one observability layer, set up replay first. Add source maps when you start triaging a lot of errors.

How it works end-to-end#

Three pieces have to line up:

  • SDK tags every event with a release identifier (a commit SHA, version tag, build ID — anything unique per deploy).
  • CLI uploads each .js.map from 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:

typescript
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:

NameTypeDefaultDescription
Next.jsNEXT_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).
ViteVITE_* via import.meta.envSet 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 AppREACT_APP_*Set REACT_APP_GIT_SHA in CI. Read in code as process.env.REACT_APP_GIT_SHA. Auto-inlined at build time.
WebpackDefinePluginnew webpack.DefinePlugin({ 'process.env.GIT_SHA': JSON.stringify(process.env.GIT_SHA) }) — replaces the literal string at build time.
Astro / Nuxt / SvelteKitPUBLIC_* / VITE_* / etc.All Vite-based — same pattern as Vite above (each enforces its own prefix; check the framework docs).

The CI side matches automatically

The CLI later sniffs 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.jsnext.config.js:

javascript
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() },
};

Vitevite.config.ts:

typescript
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

These platforms typically exclude .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:
text
# .dockerignore
node_modules
.next
dist

# explicitly include .git so build-time scripts can read the commit SHA
!.git
Same fix the CLI needs to auto-detect the release on these platforms. Once .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

A token created for "web app" cannot upload sourcemaps to "admin dashboard." If you have multiple frontend projects, generate one token per pipeline. Revoke any token that may have leaked from your dashboard at any time — verification is instant.

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:

bash
npm install --save-dev @reliableapp/frontend-cli

Now 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:

yaml
# .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:

bash
npm install --save-dev @reliableapp/frontend-cli
json
{
  "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):

json
{
  "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?

The CLI checks 14 standard env vars covering every major platform (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:

text
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

Each command below uses 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

typescript
// vite.config.ts
export default defineConfig({
    build: {
        sourcemap: true,
    },
});
bash
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

javascript
// next.config.js
module.exports = {
    productionBrowserSourceMaps: true,
};
bash
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.js puts both browser and server bundles under .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.

bash
npx --yes @reliableapp/frontend-cli sourcemaps upload \
  --dist=./build/static/js \
  --url-prefix=https://app.example.com/static/js/

Remix (v2+)

bash
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

typescript
// vite.config.ts (SvelteKit uses Vite internally)
export default defineConfig({
    build: { sourcemap: true },
});
bash
npx --yes @reliableapp/frontend-cli sourcemaps upload \
  --dist=./.svelte-kit/output/client \
  --url-prefix=https://app.example.com/

Astro

javascript
// astro.config.mjs
export default defineConfig({
    vite: { build: { sourcemap: true } },
});
bash
npx --yes @reliableapp/frontend-cli sourcemaps upload \
  --dist=./dist \
  --url-prefix=https://app.example.com/

Nuxt 3

typescript
// nuxt.config.ts
export default defineNuxtConfig({
    sourcemap: { client: true },
});
bash
npx --yes @reliableapp/frontend-cli sourcemaps upload \
  --dist=./.output/public/_nuxt \
  --url-prefix=https://app.example.com/_nuxt/

Plain Webpack

javascript
// webpack.config.js
module.exports = {
    devtool: 'source-map',  // production-quality maps; do NOT use 'eval-source-map' in prod
};
bash
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

It prints the computed asset URLs so you can verify the prefix is right without spending a real upload. If the URLs don't match what your browser fetches, resolution will silently no-op even with maps uploaded.

All CLI options#

NameTypeDefaultDescription
--token <token>$RELIABLE_TOKENAPI token (rl_fpt_...). The token already names the frontend project — no other IDs needed.
--release <id>auto-detectRelease ID. Sniffed from common platform env vars when omitted.
--dist <path>requiredLocal path to the build output folder.
--url-prefix <url>requiredBrowser-visible URL prefix that maps to the dist root.
--environment <env>productionproduction | staging | development.
--api <url>Reliable backendOverride for self-hosted backends.
--concurrency <n>4How many uploads to run in parallel.
--forceoffBypass the CI safety check (refuses to run on a developer laptop by default).
--dry-runoffWalk 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:

text
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              # generic

Privacy#

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

If your CDN also serves the .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: true in vite.config.ts.
  • Webpack: devtool: 'source-map' for production.
  • Next.js: productionBrowserSourceMaps: true in next.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 release field should equal the value the CLI logged.
  • Running the CLI with --dry-run to see which release would be used.
  • Verifying the --url-prefix produces URLs that exactly match the file URLs in your stack traces (run with --dry-run to print them).