BugTape docs
Platforms
Web, iOS, React Native, server — what each sends.
One project, one key, one inbox. Web, iOS, React Native, Android, Flutter and server failures all POST to POST /v1/ingest. The platform field is what lets the console tell them apart. On the v1 web/server path, platform is not part of the fingerprint, so the same failure can span platforms. Native v2 fingerprints hash a canonical JSON blob that includes platform plus application/build/site. Do not change that hash. A native crash does not merge with a web report of the same title.
The vocabulary
Seven values, closed set (packages/api/src/bugs/platform.ts):
| Value | Label | Typical source |
|---|---|---|
web | Web | @bugtape/sdk in a browser |
ios | iOS | BugTapeCore alpha |
android | Android | your own POST, or an OkHttp user agent |
react-native | React Native | docs/examples/react-native/bugtape.ts |
flutter | Flutter | your own POST, or a Dart user agent |
server | Server | docs/examples/server/evidence-floor.md · curl.md |
other | Other | anything else (desktop, Electron, Tauri, unknown) |
Common aliases are normalised, case-insensitively, with _/space folded to -: iPhone, ipad, swift, swiftui, uikit, macos → ios; expo, rn, reactnative → react-native; browser, js, spa, pwa → web; node, bun, deno, python, rails, go, lambda, worker, cron, cli → server; dart → flutter; electron, tauri, desktop, unknown → other. An unrecognised string becomes other rather than being rejected.
How the platform is decided
Three steps, first hit wins:
- The explicit
platformfield on the ingest payload. Always send it if you can. metadata.platform— the legacy location, still honoured.- The user agent, in this order:
| User agent contains | Platform | |---------------------|----------| | Mozilla/ | web | | CFNetwork, Darwin/, iOS, iPhone, iPad | ios | | Dalvik, okhttp, Android | android | | Flutter, Dart/ | flutter | | node, undici, axios, curl, python, Go-http-client, Java/, Ruby, PHP, Wget, Deno, Bun/ | server | | (no user agent at all) | server | | anything else | other |
A server-side hook that sends no user agent is therefore server by default, which is almost always what you want.
What each platform captures
| Web | iOS | React Native | Server | |
|---|---|---|---|---|
| Uncaught errors | yes | no fatal native capture | JS errors only, via ErrorUtils | you POST them |
| Unhandled rejections | yes | n/a | yes, via Hermes tracker | n/a |
| Handled errors | capture | client.capture(error, screen:fields:) | BugTape.capture(err, { screen }) | one POST each |
| Breadcrumbs / console | full console capture | last 100 by default, explicit hooks | last 100, manual | put them in events |
| DOM replay | yes (rrweb) | no | no | no |
| Network waterfall | yes (fetch, XHR, resource timing) | no | no | no |
| Rage clicks | yes | no | no | no |
| Device / OS | browser + OS parsed from UA | utsname model + OS version | Platform.OS/Version/Model | whatever you send |
| Release / user | yes | yes | yes | yes |
The legacy mobile examples are deliberately thin: no replay, no network interception, no symbolication, no native module. The iOS drop-in captures handled errors and manual messages only. Fatal native crash capture is unavailable pending a qualified native SDK; it installs no signal or uncaught-exception handlers. React Native JS error capture does not capture fatal native crashes. See each example's README for details.
The current Swift core package adds consent controls, a durable bounded offline queue and managed installation delivery receipts. Swift on macOS sends platform: other and osFamily: macos; iOS sends ios/ios. Neither provides native replay or fatal recording.
Field mapping
| Ingest field | Web | Non-web | Shows up as |
|---|---|---|---|
platform | web | ios / react-native / server / … | Platform facet + row tags |
url | the real page URL | the screen name: app://Checkout, worker://billing/charge | Fingerprint input, and the "where" on the occurrence |
title | TypeError: … | must be shaped <Token>Error: <message> | Issue title, and the error-kind fingerprint input |
release | build sha / deploy id | app version or build (1.4.0 (218)) | Regression detection, "last release" per user |
environment | production / staging | same | Triage filter |
userId | identify() | identify() | Affected users, user timeline |
reporterEmail | reporter | identify(email:) | User attribution when there is no userId |
sessionId | browser session | one UUID per app launch | Occurrence grouping |
device | parsed from UA | iPhone15,3, Pixel 8 | Occurrence metadata + Impact breakdown |
osVersion | parsed from UA | 17.4.1 | Occurrence metadata + Impact breakdown |
metadata | anything | bundle id, locale, your extra | Occurrence detail |
events | DOM, network, console, errors | breadcrumbs + one error event | Timeline on the bug detail |
Fingerprint by path
Web, server, and other use v1:
SHA256(errorType + ":" + normalizedMessage + ":" + urlPattern).slice(0, 16)
errorType is read off a ^(\w+Error): title prefix. urlPattern comes from url. A client that sends title: "crash" and no url gives every v1 failure the same fingerprint. Shape titles <Token>Error: <message> and send a meaningful url (app://<ScreenName>, worker://<service>/<path>).
Native (ios / android / react-native / flutter) uses v2. The fingerprint is SHA256 of a canonical JSON failureSignature that includes version, platform, applicationId, buildId, signal, and the symbol site or image offset. Title and url are not that hash. Without application/build and a symbol site or image offset, the capture isolates on its own id. Do not send a native crash expecting it to merge with a web report of the same title. Do not change the native v2 hash.
Server and React Native examples still shape title and url for the v1 path. Pass screen: on every web-style capture call.
How the console shows it
- Issues — a Platform select filters the list to one platform. Each row carries a small text tag per platform seen; an issue hit on web and iOS shows both.
- Bug detail — a Platform fact row lists every platform for that issue. The Impact tab breaks occurrences down by platform, browser and OS.
- Affected users (bug detail) — one row per end user, with a Platforms column and their last release.
- End user page / user timeline — every occurrence one person hit across every bug, with a Platform column per row.
- Setup — the snippet tabs (npm, Script tag, Next.js, iOS, React Native, Server / curl) each carry a platform, and Send console sample (
POST /v1/projects/:id/test-event) mints a real occurrence tagged with the tab you picked, through the normal pipeline, before you install anything. Console samples and aggregate report counts do not verify client installation.
How agents see it
Over MCP (see agents.md):
list_bug_groups { platform }— grouped triage, filtered to groups with an occurrence on that platform. Each group returns aplatformsarray.get_bug_users { bugId }— one row per affected end user, withplatforms,lastReleaseandlastOccurrenceId.get_user_timeline { userKey }— everything one user hit across bugs and projects, withplatform,releaseandenvironmenton each occurrence.
Use the platform filter to find groups with iOS reports. Groups may also contain other platforms; this filter does not mean "iOS-only" or prove fatal crash capture.
Design rule
v1 web/server: platform is not in the fingerprint. The same failure from web and a worker stays one issue with two platforms.
Native v2: platform is in the fingerprint. That is the live hash. Do not change it to match the v1 sentence. Filter by platform when you want to; do not expect a native crash and a web report of the same title to be one issue.
Where to go next
- Swift core alpha — package download, consent, handled reports and managed setup
examples/react-native/— one TypeScript file, Expo and bare RNexamples/server/evidence-floor.md— required fields, MCP packet, Streamlit guidanceexamples/server/curl.md— curl, Node and Python recipesexamples/server/bigquery-timeout.md— redacting Python helper + BigQuery timeoutprivacy-proof-checklist.md— before-key DOM scrub proofingest-schema.md— every field, limit and error responseagents.md— how your coding agent hears about new bugs