Start here
Core concepts
The handful of ideas every SDK, the CLI and the dashboard share. Learn these once and every platform page reads the same.
Capsule
A .bugcapsule file is gzip-compressed UTF-8 JSON with three parts:
- manifest: who, what, where. App version/build, git SHA, platform, runtime, session, the failure, eligibility, integrations, labels, the redaction rules used.
- checkpoint: the state replay starts from (storage, route, registered state).
- events: an ordered window of what happened after the checkpoint: touches, actions, text, lifecycle, every HTTP exchange with its latency, state-manager activity, logs, the failure.
Replay re-runs the real app code against that recording. Network calls are served from the capsule with the recorded timing, so races and data-dependent bugs come back. Full spec: Capsule format.
Flight recorder and capture
While a session records, the SDK keeps a bounded in-memory window: 5 minutes or 20,000 events. Nothing is written until a failure happens (or QA presses capture). Then the SDK writes a capsule (300 ms later, to let the failure settle; crashes immediately), redacts it, saves it locally and queues it for upload.
| Trigger | Failure kind | Notes |
|---|---|---|
| Uncaught error / crash | exception | Boundaries differ per runtime (see each SDK page) |
check(id, condition) / fail(id) | assertion | Signature assertion:<id>: everywhere |
reportError(e) | exception | Errors your code caught |
Manual capture(note) | the last failure, or none | Without a failure the capsule is ineligible |
At most 3 automatic capsules per session, one per failure signature.
Capture modes
| Mode | Records | Typical build |
|---|---|---|
debug | Always | Local development |
qa | Always | QA and internal testing builds |
production | With probability min(productionSampleRate, remote sampleRate), decided at launch (default cap 10%) | Store builds |
Defaults when you don't set a mode: Flutter kReleaseMode ? production : debug, Android debuggable → debug, iOS DEBUG → debug, web always production. Sessions that aren't sampled install no recorders.
Checkpoints
- A launch checkpoint is taken before the app starts (before
runApp, beforeApplication.onCreate, instart()/init()). Replay from it re-executes the whole session; only storage needs restoring. It's the most reliable kind and is used whenever the session fits in the window. - A periodic checkpoint (every 30 s) also carries registered state and the current route. It's used only when the session outgrows the window.
Storage is captured only from an allowlist you configure, and passes through redaction. This is why init must run first: in replay it restores the checkpoint before app code reads storage.
Eligibility
The device computes it when writing the capsule, and lists every gap in eligibility.missing:
| Status | Meaning | Common causes |
|---|---|---|
| eligible | Everything replay needs is there | |
| partial | Replayable, with known gaps | No git SHA, a fixture without a body, crash.persisted, a capability the SDK lacks |
| ineligible | A blocking gap | No failure assertion (manual capture) |
Ineligible capsules are still stored and inspectable, but never counted in the reproduction rate.
Making a bug replayable
| Concept | What it buys you |
|---|---|
Semantic actions: wrap a handler with an id (cart.checkout) | Replay invokes it by id even if the developer's screen differs. Required on iOS (no pointer replay). |
| Tracked text: register an input with an id | Typed text is restored by id. Sensitive fields record [REDACTED]. |
Assertions: check / fail | Wrong behaviour that doesn't throw becomes a failure replay can detect. |
| Deterministic hooks: SDK clock and random | Replay returns the recorded wall clock and seed. Only the SDK hooks are virtualized; Date.now(), timers and animations are not. |
| Git SHA at build time | bugcapsule run checks out the exact commit that failed. |
Request keys and endpoint rules
Each HTTP exchange is stored under a normalized key: METHOD host path?sortedQuery#bodyHash, plus an occurrence (0-based count of earlier requests with the same key). Replay serves the fixture with the same key and occurrence, after the recorded latency. Every runtime computes identical keys (checked by shared conformance fixtures).
Fields that change on every run (timestamps, nonces, request ids) would break matching, so drop them with an endpoint rule:
EndpointRule('POST', '/api/checkout', ignoreBodyFields: {'timestamp', 'nonce'})
Query params _ ts timestamp nonce cachebust are ignored by default. Rules can also restrict hashing to matchBodyFields. Bodies up to 64 KiB are hashed (never stored for the key); multipart is never hashed.
Replay is strict by default: a request the recording never saw fails like a connection error and reports network.unrecorded. Nothing reaches real services. --lenient-network lets it through but still reports it.
Credentials
| Credential | Prefix | Who holds it | Can do |
|---|---|---|---|
| SDK token | bcsdk.v1. | The app (public) | Read capture config, upload capsules and monitoring data. Carries the BugCapsule address. Rotate with a grace period, or revoke |
| Ingest key | bci_ | The app, in setups from before SDK tokens (public) | Read capture config, upload capsules |
| Secret key | bcs_ | The CLI and CI (never an app) | List, download, replay and triage your project's capsules; change capture settings |
| Share token | in URL | Anyone with the link | Download one capsule until expiry |
Capsule status
new ──run: reproduced/flaky──► reproduced ──verify: clean in every attempt──► fixed
│ │
└─run: clean in every attempt──► not_reproduced ineligible (on upload) archived (manual)
- A capsule starts as
new, orineligiblewhen its eligibility is ineligible. fixedandnot_reproducedneed every requested attempt to have run validly (validAttempts == attempts).fixedalso requires the capsule to bereproducedfirst.inconclusive,error,incompatibleresults never change the status.
Verdicts
The CLI judges each attempt (reproduced, clean or invalid), then the batch:
run | verify | Exit |
|---|---|---|
| REPRODUCED | FIXED | 0 |
| NOT REPRODUCED | STILL FAILING | 1 |
| FLAKY | FLAKY | 2 |
| INCONCLUSIVE / INCOMPATIBLE | INCONCLUSIVE / INCOMPATIBLE | 3 |
Attempts that never ran count against the request: one clean attempt plus one error is INCONCLUSIVE, never FIXED. Details and the divergence catalogue: Verdicts & divergences.
Remote config and the kill switch
Each device fetches your project's capture settings after startup, then every few minutes (default 5) and when the app returns to the foreground. You change them with bugcapsule capture or in the dashboard: capture on/off, sample rate, maximum capsule size, body capture, extra redaction keys and the refresh interval. They can only make capture more conservative: lower sampling, turn bodies off, add redaction, or switch capture off.
Nothing is uploaded until the settings have been fetched once (or a cached copy is younger than 7 days). The kill switch stops recording, drops the buffer, deletes queued and saved capsules, and persists across launches.
Glossary
- Anchor
- The first rendered frame; event
tis milliseconds since it (negative before) - Divergence
- Something replay couldn't reproduce exactly; material ones invalidate a clean attempt
- Fixture
- A recorded HTTP response served during replay
- Integration
- A plug-in (HTTP client, router, storage, state manager) that records events and owns one checkpoint key
- Outbox
- On-device upload queue: 10 capsules / 10 MiB / 7 days
- Runtime
flutter,android,iosorweb- Signature
<kind>:<type>:<normalized message>, the key replay compares failures by