Copy page
Source maps
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 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 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.
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.
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 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.