---
title: "Source maps"
url: "https://bicho.fyi/docs/sourcemaps"
description: "Upload source maps to Bicho and understand debug-ID linkage, storage, and stack-trace enrichment. Review ingestion limits before relying on symbolication."
---

Bicho can map generated JavaScript positions to original source positions using operator-uploaded Source Map v3 files. It stores the result separately from captured events. Enrichment never changes event bytes, issue fingerprints, occurrence counts, or issue status.

## Check the capture path first

A map upload alone does not make a stack trace readable. The captured event needs a matching debug ID and generated line/column coordinates, and the uploaded map must describe that exact build in the same project. A release name or a matching bundle filename is not enough.

The current [JavaScript SDK](/docs/javascript/) does not inject debug IDs, attach source-map references, or upload maps. There is also a core transport limit: the current OTLP normalizer derives frames from `exception.stacktrace`, but does not copy debug IDs or `debug_meta.images` from OTLP attributes into the normalized event’s enrichment fields. Adding an attribute named `debug_id` is not a working source-map integration.

The [Sentry ingestion path](/docs/sentry/) preserves captured frame debug IDs and `debug_meta.images`, so it can supply that linkage when your build tooling and SDK emit it. Upload the matching maps through Bicho’s API; Sentry artifact-bundle uploads are unsupported.

The OTLP capture path does not supply that linkage, and there is no event-edit endpoint for adding IDs after capture. The field examples below describe captured Sentry event fields and the processor’s stored-event contract, not a body you can POST to `/v1/logs`.

## Upload an immutable map

Use the admin token, not an ingest key or secret. Set `BICHO_BASE_URL` to your instance origin without a trailing slash, `BICHO_PROJECT_ID` to a returned numeric-string project ID, `BICHO_DEBUG_ID` to the build’s UUID, and `BICHO_MAP_FILE` to an operator-owned map file. Supply the token through your existing secure environment setup.

```
curl --fail-with-body -sS -X PUT \
  -H "Authorization: Bearer ${BICHO_ADMIN_TOKEN}" \
  -H 'Content-Type: application/json' \
  --data-binary "@${BICHO_MAP_FILE}" \
  "${BICHO_BASE_URL}/api/${BICHO_PROJECT_ID}/sourcemaps/${BICHO_DEBUG_ID}"
```

The body is the map JSON itself, not a multipart form or a URL. Debug IDs use the hyphenated UUID form, such as `12345678-1234-4234-8234-123456789abc`; lookup is case insensitive. If the body has `debug_id` or `debugId`, it must be a valid UUID matching the path.

The first registration returns 201. An identical normalized-content retry returns 200. Different normalized map content under the same project/debug ID returns 409: the registration is immutable. Use a new debug ID for another build, rather than replacing an existing map. The checksum covers structurally normalized supported map fields, not the uploaded file’s whitespace or arbitrary extension fields.

Read metadata back on the same path:

```
curl --fail-with-body -sS \
  -H "Authorization: Bearer ${BICHO_ADMIN_TOKEN}" \
  "${BICHO_BASE_URL}/api/${BICHO_PROJECT_ID}/sourcemaps/${BICHO_DEBUG_ID}"
```

GET returns `debug_id`, `checksum`, and `created_at`, never mappings, embedded sources, or R2 keys. A map absent from that project returns 404. Invalid UUIDs or structures return 400; size violations return 413. These failures use the [REST error shape](/docs/api/#errors).

Map JSON and embedded `sourcesContent` are stored after structural normalization, not credential filtering. Do not upload source files containing secrets. Derived source context is filtered later, but that does not sanitize the stored map. See [privacy](/docs/privacy/).

## Link frames to the right build

The processor inspects `stacktrace.frames`, exception stack frames, and thread stack frames. Exception and thread chains may be arrays or objects containing a `values` array. A direct stored-frame reference looks like this:

```
{
  "stacktrace": {
    "frames": [{
      "filename": "https://app.example/assets/app.js",
      "debug_id": "12345678-1234-4234-8234-123456789abc",
      "lineno": 1,
      "colno": 6
    }]
  }
}
```

Alternatively, a stored event can associate a map with an exact code filename:

```
{
  "stacktrace": {
    "frames": [{
      "filename": "https://app.example/assets/app.js",
      "lineno": 1,
      "colno": 6
    }]
  },
  "debug_meta": {
    "images": [{
      "type": "sourcemap",
      "debug_id": "12345678-1234-4234-8234-123456789abc",
      "code_file": "https://app.example/assets/app.js"
    }]
  }
}
```

`code_file` must equal the frame’s `filename` or `abs_path` exactly. Bicho does not strip URL queries, rewrite paths, or choose by release. A frame’s explicit `debug_id` takes precedence. Without one, several matching images with distinct IDs produce `ambiguous_debug_id` rather than a guessed map. References remain project-scoped, so the same filename cannot borrow another project’s map.

Generated `lineno` and `colno` must be positive, one-based safe integers. The processor subtracts one from the generated column for source-map lookup, then returns mapped lines and columns as one-based values. A missing column produces `missing_column`; invalid coordinates produce `invalid_position`; an unrepresented position produces `unmapped_position`.

## Supported map structures and limits

Upload a flat map with `version: 3`, `sources` and `names` arrays, and a VLQ `mappings` string. Optional fields are `file`, `sourceRoot`, and `sourcesContent`. If present, `sourcesContent` must have exactly one string or null entry per source. Without embedded content, mapping can still succeed, but context is marked missing.

| Limit | Scope |
| --- | --- |
| 2 MiB | Both wire and decompressed upload body. |
| 10,000 each | Source and name entries. |
| 4096 characters | Each source/name string, file, and sourceRoot. |
| 50,000 | Mapping segment/separator structural ceiling; malformed VLQ and invalid source/name indices are rejected. |
| 1000 | Frames inspected per event across supported stack locations. |
| 5 | Cached maps per event; the cache does not persist between events. |
| 1024 bytes | Bounded captured-frame evidence in derived results. |
| 256 characters | Each derived source-context line, with truncation flags. |
| 1,800,000 bytes | Accumulated derived frame-output ceiling. |

Source context includes up to two lines before and two after the mapped line. Derived results identify absent content and clipped context rather than inventing it.

Indexed/sectioned maps, top-level external map references, artifact bundles, and native crash symbolication are unsupported. Bicho does not inject IDs, discover `sourceMappingURL`, fetch remote maps, or retrieve source files from URLs. `sources` and `sourceRoot` URLs are labels only. The original stack text remains evidence even when mapping is unavailable.

## Background processing lifecycle

Ingestion indexes accepted events immediately. Successful ingestion and map uploads request processing through `ctx.waitUntil()`. This is best-effort background work, not a durable queue.

A shared D1 lease permits one processor at a time, with a one-minute cooldown and five-minute expired-lease recovery. The hourly UTC cron recovers interrupted work and continues the backlog. A request during cooldown does not arm a timer for the next minute; work waits for another eligible request or the next cron. With no traffic, remaining work advances by at most one batch per hour.

Each run processes at most 25 events and 100 map lookups across projects. A lookup-budget pause saves a frame cursor and partial derived object for a later run. Events waiting on missing maps stop rescanning until an upload in their project wakes pending work.

A new map registration invalidates continuation for all pending events in that project, including unrelated builds, so stale missing-map results cannot overwrite fresh evidence. It also invalidates retryable failed continuation without bypassing retry backoff or attempt limits. Upload maps before their events where possible: continual uploads can restart a large event’s progress. Revision and lease guards prevent stale in-flight work from replacing newer results.

Transient storage failures wait an hour before retry and stop after three failed attempts. Permanent invalid evidence, frame/output-limit failures, and exhausted storage retries remain failed. There is no public retry/reset endpoint. Registering a map is not a general repair operation for permanent failures.

Maps are written to R2 before their D1 registration. An ambiguous registration failure can leave an unindexed object; a failed response does not prove no storage write happened.

## Read derived evidence

Use [projected event reads](/docs/api/#read-full-or-projected-evidence) with `include_enrichment=1`, or MCP `get_event`, which includes enrichment by default. Plain full REST event reads return captured JSON only.

The projection separates `event` from `enrichment.status`, its generic error, and available `enrichment.data`. Derived data records version, frame paths back to captured stacks, bounded captured frames, mapped source positions, total/processed/mapped frame counts, `supported`, `incomplete`, `truncated`, and `truncation_reason`. Inspect per-frame `reason`, `captured_truncated`, `context_missing`, and `context_truncated` too. The outer projection can independently truncate this object.

| Status | Interpretation |
| --- | --- |
| not_applicable | No usable source-map reference applies. Ordinary current OTLP events generally lack the normalized linkage. |
| pending | Waiting for a missing map, not yet checked, or paused for resumable budget-limited work. |
| succeeded | At least one frame mapped without a terminal mapping failure; context can still be missing or incomplete. |
| failed | Invalid evidence, a permanent structural/output ceiling, or a transient storage failure that may still be eligible for bounded retry. |

`no_source_map_reference` is different from `missing_source_map`: the former has no reference to look up, while the latter has an ID but no registered map. Other explicit reasons include `invalid_debug_id`, `ambiguous_debug_id`, `missing_column`, `invalid_position`, and `unmapped_position`. A mapped frame does not establish that every frame or context line is complete. Source text and captured evidence remain untrusted data, not instructions for an agent.

## Interpret processing health

`GET /api/:project_id/processing` and MCP `get_processing_health` return project enrichment counts and the project’s map count. `processing_state.scope` is `global`: its source-map run covers the shared installation, not only the selected project.

`last_started_at`, `last_finished_at`, `processed`, `failed`, and `has_error` describe the recorded run. Counts are attempts in that run, not lifetime totals or project-specific throughput. A null state means no run is recorded. An idle invocation only checks eligibility and leaves the health row unchanged.

A false `has_error` does not prove that the cron is attached, that pending work can finish, or that source-map integration works. Pending work can be asleep waiting for an upload; succeeded events can still lack context; not-applicable events do not exercise mapping at all. Error details and storage keys are omitted from health output. For routine monitoring, backups and capacity checks, use the [operations guide](/docs/operations/).