bugwatch docs

Issues

For agents: search_issues(org="acme", project="web", status="unresolved", q="timeout", sort="last_seen", limit=10) to find, get_issue for a summary, get_fix_context before fixing, resolve_issue / ignore_issue / unresolve_issue / bulk_update_issues to change status (event:write, audited), get_issue_activity for what people did to it, get_issue_history for how often it has actually happened.

Grouping (fingerprint v2)

Each processed error event gets a deterministic fingerprint; events with the same fingerprint are one issue. In priority order:

  1. SDK fingerprint (event.fingerprint) when set, with {{ default }}, {{ transaction }}, {{ message }}, and {{ tag.<name> }} expanded.
  2. Exception with stack trace — for up to 5 chained exceptions (root cause first): the exception type plus the ordered frames, using in_app frames when any exist (otherwise all), capped at the last 25. Each frame contributes module (or the filename basename with build hashes such as -8f3a9c1e stripped) and the platform-normalised function name; one- or two-character minified names are dropped, direct recursion is collapsed. The exception message is never included — it carries ids, timestamps, and user data.
  3. Exception without a stack — type + the normalised message (numbers, hex, UUIDs, emails, and URLs replaced by placeholders).
  4. Message events — the logentry.message template (not the formatted string) + logger.
  5. Fallback — platform + level + transaction.

hash = sha256("v2" + material)[0:32]. The version is part of the hash, so a future v3 applies to new issues only; nothing existing splits.

Title is Type: value (truncated to 120 chars) or the message; culprit is the top in-app frame as function (module) or the transaction name.

Status lifecycle

Status Meaning
unresolved Default for new issues and for reopened ones
resolved Marked fixed. Optionally in a release
ignored Muted: no alerts, hidden from the default list
regressed Was resolved, then reproduced — see below; alert rules can match on it

Change status with PATCH /v1/orgs/{org}/projects/{project}/issues/{shortId} {"status": "…"}, or up to 100 at once with POST …/issues/bulk {"shortIds": [42, 43], "status": "resolved"} (bulk allows unresolved, resolved, ignored). Both need event:write and record who did it (user or token).

Regressions

A new event for a resolved issue flips it to regressed and emits an issue.regressed alert signal. When the issue was resolved in a release, only events whose release is semver-newer than that release regress it; events from the same or an older release (a straggling old deploy) do not, and incomparable version strings never regress. The state lives in the per-project coordinator and is mirrored to the control database.

Counts

eventCount is exact: it comes from the project coordinator's counters, flushed to the control database, and is what billing is derived from.

userCount is an estimate and is labelled as one everywhere it appears. Exact distinct users would mean keeping every user hash per issue, and the storage contract reserves exact counters for the numbers that must never be estimated. Instead a nightly job asks the analytics store for distinct user hashes per issue over the last 30 days and writes that number back. Under sampling it is a lower bound, and events with no identified user are excluded rather than collapsed into one phantom user. An issue seen for the first time today shows its estimate after the next nightly run.

Search and listing

GET /v1/orgs/{org}/projects/{project}/issues accepts:

  • status — one of the four statuses (omit for all).
  • q — up to 100 characters, literal substring match on title or culprit (wildcards are escaped).
  • sort — last_seen (default) or first_seen, newest first.
  • limit — 1–100 (default 25), cursor — opaque, from nextCursor.

Counts in the list (eventCount, userCount) are exact. userCount is distinct salted user hashes, not raw identities.

Inline summary

In the dashboard, the chevron on a row (or Space on the focused row) expands it in place without leaving the list. The summary is the latest stored event, fetched only when the row is opened, laid out as tabs:

  • Stack — the exception chain, outermost first, with frames innermost first. Frames that carry source context show the surrounding lines with syntax highlighting and the failing line marked; frames with locals show them beneath. Library frames are hidden by default when in-app frames exist (In-app only · N hidden toggles them).
  • Breadcrumbs — the timeline, newest first; a fetch that came back 4xx/5xx is marked failed whatever level it claimed.
  • Request — method, URL, query, headers, body and env from the event's request.
  • Context — tags as chips, then user, browser/OS/runtime/trace contexts and extra data.

Every tab keeps the same actions: Fix with agent, Copy as Markdown, Open issue. The issue page renders the same frame rows, so a stack reads identically in both places.

Events

GET …/issues/{shortId}/events lists stored events for the issue (newest first, 50 per page, R2-backed); GET …/events/{eventId} returns the full processed body — scrubbed, symbolicated, with breadcrumbs, contexts, tags, request, and SDK info. Event ids are 32 hex characters.

Fix context

GET …/issues/{shortId}/fix-context (tool get_fix_context) assembles, from the latest event:

  • event.exceptions[] — type, value, and the last 5 in-app frames with file, function, line, col, the context line and 3 lines of pre/post context;
  • event.breadcrumbs — the last 10, trimmed to category, message (200 chars), level; event.symbolication flags frames that could not be resolved;
  • release — version, commit SHA and URL when registered;
  • issue — title, culprit, level, status, first/last seen, impact (exact events, approximate users), deep link;
  • similarResolved — issues of the same exception type this project already resolved, with their links.

It is one request and a few KB, which is why the fix_issue prompt calls it before anything else.

Activity feed

GET …/issues/{shortId}/activity returns the last 50 entries: status changes with the actor (user email or token), regressions, and mcp_action rows written when an agent changes the issue, each carrying the tool name and token id.

How long it has been happening

The chart on the issue page comes from Analytics Engine, which keeps three months. That answers "is this spiking now" and cannot answer "was this happening last spring" — and for a recurring issue that second question is the one that decides whether you are looking at a regression or at something that has been quietly failing all along.

GET /v1/orgs/{org}/projects/{project}/issues/{shortId}/history?days=365 (tool: get_issue_history) answers it from the nightly rollup, which has one row per issue-day and is kept for 400 days — thirteen months, so "this time last year" is in range.

365 days requested, 47 with activity since 2025-09-09, through 2026-09-08 · 12,904 events (estimated)
busiest day 2026-03-02: 2,110 events, ~430 users

Two things to know about the numbers:

  • They are estimates. The rollup is derived from the sampled analytics index, so it carries that sampling with it; the response says estimated: true rather than leaving you to infer it. What the rollup adds is durability past the analytics window, not precision. Exact counts live where they must — the billing meter and the issue counters — and never come from here.
  • Today is not in it. The rollup runs nightly, so the newest day is yesterday. through says which day the history actually reaches, rather than leaving a chart that always ends a day short with nothing explaining why.

A day with no activity has no row at all, so an issue that fired twice in a year returns two entries rather than 363 zeros.

Resolving from a commit

POST …/resolve-by-commit (tool resolve_issues_from_commit) reads a commit message and resolves the issues its trailers close — Fixes #42, Closes COMPAT-42 — recording the commit on each issue's activity feed. Wire it into CI on your default branch and merging a fix is what closes the issue.

A closing verb is required: see #42 is a mention and resolves nothing. The fix agent writes that trailer into the pull requests it opens.

Deduplication

The coordinator remembers event ids for 24 hours; a redelivered or replayed event does not create a second copy or increment counters twice.