> ## Documentation Index
> Fetch the complete documentation index at: https://docs.aiaxoniq.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Source maps for browser errors

> Upload your build's source maps so browser errors show the original file, line and column — and the two rules that make an upload match an error.

A browser reports an error against your **minified** bundle — `app.a91f.js:1:36`
tells you nothing. When you upload the source map your build produced, the
error detail in **RUM → Errors** shows the original position instead, for
example `../../src/cart/total.ts:16:23`, beside the minified one it was resolved
from. The file is shown exactly as your source map's `sources` list names it —
often, as here, a path relative to the map.

Resolution happens when you open an error, against whatever map is uploaded at
that moment. You can upload a map after the errors it describes have already
arrived.

## Before you start

You need three things from a **Browser** project. The first two are read from
the API with a session token — see [Authentication](/get-started/authentication)
for how to get one.

Every call below also names your organization as `tenantId`; without it the API
answers `401 Missing tenantId`. Read your organization id from your session, then
find the project:

```bash theme={null}
TENANT_ID=$(curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  https://app.aiaxoniq.com/auth/me | jq -r .user.tenantId)

curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  "https://app.aiaxoniq.com/api/projects?tenantId=$TENANT_ID" |
  jq -r '.[] | select(.type == "Browser") | "\(._id)  \(.name)"'
```

The first column is the project id; set `PROJECT_ID` to it.

**The install snippet.** Paste it into the `<head>` of every page:

```bash theme={null}
curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  "https://app.aiaxoniq.com/api/projects/$PROJECT_ID/rum-snippet?tenantId=$TENANT_ID"
```

**The project admin key.** It is the `adminApikey` field of the project,
returned to members with the **Editor** role or above:

```bash theme={null}
curl -s -H "Authorization: Bearer $SESSION_TOKEN" \
  "https://app.aiaxoniq.com/api/projects/$PROJECT_ID?tenantId=$TENANT_ID" | jq -r .adminApikey
```

<Warning>
  The admin key can upload, replace and delete this project's source maps.
  Store it as a secret in your CI system, never in your repository or in the
  page. If it is exposed, a member with the **Admin** role can replace it — the
  old key stops working immediately:

  ```bash theme={null}
  curl -s -X POST -H "Authorization: Bearer $SESSION_TOKEN" \
    "https://app.aiaxoniq.com/api/projects/$PROJECT_ID/change-apikey?admin=true&tenantId=$TENANT_ID"
  ```

  The response contains the new `adminApikey`; put it in your CI secret in place
  of the old one. The `apikey` in your install snippet does not change, so your
  pages keep reporting.
</Warning>

**A release id** your build knows — a version, a build number, or a commit
hash. It must be the same value in the page and in the upload.

## 1. Declare the release in the page

Add this after the install snippet, with your own name and release id:

```html theme={null}
<script>
  window.AIAXONIQ.addRelease('shop-frontend', '2026.09.11+build.42');
</script>
```

The first argument names the application; the **second** is the release id
you upload maps under. A page can declare up to ten releases. Surrounding
whitespace is ignored, and an id longer than 200 characters keeps its last 200.

A page that never calls `addRelease` still reports errors, but none of them can
be matched to an uploaded map.

## 2. Upload the maps from your build

Run this from your build pipeline after the bundle is built, with the admin key
in `AIAXONIQ_ADMIN_KEY`. It uploads every `*.js.map` under the directory you
name, and exits non-zero if any upload fails or no map is found:

```bash theme={null}
#!/usr/bin/env bash
# Upload every JavaScript source map in a build directory to aiAxonIQ.
#
#   AIAXONIQ_ADMIN_KEY=<project admin key> ./upload-sourcemaps.sh \
#     --api https://app.aiaxoniq.com \
#     --project <project id> \
#     --release <the id your page passes to addRelease> \
#     --dir dist \
#     --url-prefix /
#
# Each <file>.js.map under --dir is uploaded as the map for the script served
# at <url-prefix>/<its path under --dir, without .map>. Only the URL path is
# used to match errors, so --url-prefix may be a full URL
# (https://cdn.example.com/static) or a path (/static).
#
# Exit status: 0 every map uploaded, 1 any upload failed or no map was found,
# 2 a required argument or AIAXONIQ_ADMIN_KEY is missing.
set -euo pipefail

api="" project="" release="" dir="" prefix="/"
while [ $# -gt 0 ]; do
  case "$1" in
    --api) api="$2"; shift 2 ;;
    --project) project="$2"; shift 2 ;;
    --release) release="$2"; shift 2 ;;
    --dir) dir="$2"; shift 2 ;;
    --url-prefix) prefix="$2"; shift 2 ;;
    -h | --help) sed -n '2,17p' "$0"; exit 0 ;;
    *) echo "unknown argument: $1" >&2; exit 2 ;;
  esac
done

if [ -z "$api" ] || [ -z "$project" ] || [ -z "$release" ] || [ -z "$dir" ]; then
  echo "--api, --project, --release and --dir are required (see --help)" >&2
  exit 2
fi
if [ -z "${AIAXONIQ_ADMIN_KEY:-}" ]; then
  echo "AIAXONIQ_ADMIN_KEY is not set: export the project's admin key first" >&2
  exit 2
fi

maps=()
while IFS= read -r -d '' map; do maps+=("$map"); done < <(find "$dir" -type f -name '*.js.map' -print0 | sort -z)
if [ "${#maps[@]}" -eq 0 ]; then
  echo "no *.js.map files under $dir: is that your build output directory?" >&2
  exit 1
fi

failed=0
for map in "${maps[@]}"; do
  relative="${map#"${dir%/}"/}"
  url="${prefix%/}/${relative%.map}"
  response="$(mktemp)"
  # The key reaches curl on stdin, never on its command line, so it does not
  # appear in the process list or in a CI job's echoed commands.
  status="$(printf 'header = "X-Admin-Key: %s"\n' "$AIAXONIQ_ADMIN_KEY" |
    curl --silent --show-error --config - \
      --output "$response" --write-out '%{http_code}' \
      --form-string "release=$release" \
      --form-string "url=$url" \
      --form "file=@$map;type=application/json" \
      "${api%/}/api/projects/$project/sourcemaps")" || status="000"
  if [ "$status" = "200" ]; then
    echo "uploaded $url $(cat "$response")"
  else
    echo "FAILED $url (HTTP $status) $(cat "$response")" >&2
    failed=1
  fi
  rm -f "$response"
done
exit "$failed"
```

For a build whose `dist/assets/app.a91f.js` is served at
`https://cdn.example.com/static/assets/app.a91f.js`:

```bash theme={null}
AIAXONIQ_ADMIN_KEY="$ADMIN_KEY_FROM_CI_SECRETS" ./upload-sourcemaps.sh \
  --api https://app.aiaxoniq.com \
  --project "$PROJECT_ID" \
  --release "2026.09.11+build.42" \
  --dir dist \
  --url-prefix https://cdn.example.com/static
```

Each successful upload prints the key it was stored under. To upload a single
map without the script — the key is passed on standard input, as the script
does, so it never appears in your machine's process list:

```bash theme={null}
printf 'header = "X-Admin-Key: %s"\n' "$AIAXONIQ_ADMIN_KEY" | curl -s --config - \
  --form-string "release=2026.09.11+build.42" \
  --form-string "url=/static/assets/app.a91f.js" \
  --form "file=@dist/assets/app.a91f.js.map" \
  "https://app.aiaxoniq.com/api/projects/$PROJECT_ID/sourcemaps"
```

## How an upload is matched to an error

An error is resolved against a map only when **both** of these match.

|             | The error carries                             | The upload must send                                                  |
| :---------- | :-------------------------------------------- | :-------------------------------------------------------------------- |
| **File**    | The URL of the script the error was thrown in | `url` — the URL or path of the **minified script**, not of the `.map` |
| **Release** | Every release id the page declared            | `release` — one of those ids                                          |

Only the **path** of `url` is compared. The host, query string and fragment are
ignored, so these all name the same file:

```text theme={null}
https://shop.example.com/assets/app.a91f.js
https://cdn.example.net/assets/app.a91f.js?v=7
/assets/app.a91f.js
```

Paths are compared whole, so `/admin/main.js` and `/shop/main.js` never share a
map. A `url` ending in `.map` is refused with `400`, as is a file that is not a
version 3 source map.

Uploading again for the same file and release **replaces** the map, and the
next error you open uses the new one.

## Reading the result

Expand an error in **RUM → Errors**. Under **First script frame** you see either
**Symbolicated**, with the original position, the minified position and the
release whose map was used — or **Not symbolicated**, with the reason:

| The reason says                                                                              | What to do                                                                                                                                            |
| :------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------- |
| source map resolution is disabled for this project                                           | The project's `ignoreSourcemap` setting is on. Your uploaded maps are kept and accepted while it is on; set it to `false` to resolve errors with them |
| the page declared no release                                                                 | Call `addRelease` in the page (step 1)                                                                                                                |
| no source map uploaded for `<path>` under release `<id>`                                     | Upload with exactly that path and release — compare with the response your upload printed                                                             |
| different source maps for `<path>` were uploaded under more than one of this page's releases | Give each application its own release id, or stop uploading one path under two releases                                                               |
| position `…` is outside the source map                                                       | The map is from a different build of that file than the page ran                                                                                      |
| source map lookup failed                                                                     | Open the error again; this is a temporary failure on our side                                                                                         |

<Note>
  Only the first frame that is in a script file is resolved, not the whole
  stack. An error from a script loaded from another origin must be allowed to
  report its details: load that script with `crossorigin="anonymous"` and serve
  it with an `Access-Control-Allow-Origin` header, or the browser hides the
  error as `Script error.` with no stack to resolve.
</Note>
