Skip to content

Commit c9f7fe7

Browse files
committed
chore: add Bugsnag triage skill
Port the /triage Claude skill from iOS to Android. Fetches the top open production Bugsnag issue, investigates stack traces/logs/breadcrumbs, proposes a fix direction, routes through domain experts, and writes a lean review brief. - SKILL.md adapted for Kotlin/Android (stack frame mapping, version check via release-manifest.json, Android expert routing) - event-shape.md documents Android Bugsnag event structure - bugsnag-top.sh fetches top error with retry/rate-limit handling - .gitignore updated to cover .env and .env.local Signed-off-by: Brandon McAnsh <git@bmcreations.dev>
1 parent 5c82006 commit c9f7fe7

5 files changed

Lines changed: 468 additions & 0 deletions

File tree

.claude/skills/triage/SKILL.md

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
---
2+
name: triage
3+
description: >
4+
Fetch the top open Bugsnag production issue, investigate with evidence from
5+
stack traces / logs / breadcrumbs, propose a fix direction, route through
6+
domain experts, and write a lean review brief.
7+
allowed-tools:
8+
- Bash
9+
- Read
10+
- Glob
11+
- Grep
12+
- Write
13+
- Edit
14+
- Agent
15+
- WebFetch
16+
- Skill
17+
---
18+
19+
# Triage: daily Bugsnag issue investigation
20+
21+
You are a senior Android engineer performing daily Bugsnag triage for the
22+
Flipcash Android app. Follow the steps below exactly.
23+
24+
## Step 1 — Fetch the top open issue
25+
26+
Run the helper script:
27+
28+
```bash
29+
bash .claude/skills/triage/scripts/bugsnag-top.sh
30+
```
31+
32+
The script emits a single JSON object with:
33+
34+
| Field | Description |
35+
|-------|-------------|
36+
| `error_id` | Bugsnag error group ID |
37+
| `error_url` | Deep link into the Bugsnag dashboard |
38+
| `event_url` | REST API URL for the latest event |
39+
| `title` | Error class + message |
40+
| `severity` | `error` / `warning` / `info` |
41+
| `events` | Total occurrences |
42+
| `users` | Unique affected users |
43+
| `first_seen` | ISO-8601 timestamp |
44+
| `release` | `app_version` from the latest event |
45+
46+
If the script exits non-zero, stop and report the error to the user.
47+
48+
## Step 2 — Pull event detail
49+
50+
Fetch `event_url` (include header `Authorization: token $BUGSNAG_TOKEN`).
51+
Source `.env` from the repo root if the variable is not already set.
52+
53+
Parse the response using the event shape documented in
54+
`.claude/skills/triage/references/event-shape.md`.
55+
56+
Extract four evidence sources:
57+
58+
1. **Stack trace**`exceptions[0].stacktrace` (frames with `file`, `method`,
59+
`lineNumber`, `inProject`)
60+
2. **Exception info**`exceptions[0].errorClass` + `exceptions[0].message`
61+
3. **App logs**`metaData["App Logs"]["app_log"]` (single string, last ~64 KB
62+
of log output)
63+
4. **Breadcrumbs**`breadcrumbs[]` (timestamped UI / state / network events)
64+
65+
## Step 3 — Map stack frames to source
66+
67+
Android stack frames use Java/Kotlin package-qualified class names (e.g.
68+
`com.flipcash.features.cash.CashViewModel`). To locate the source file:
69+
70+
1. Convert the class name to a path fragment: replace `.` with `/` and append
71+
`.kt` (try `.java` as a fallback).
72+
2. Use `Glob` to find the file in the repo (e.g. `**/**/CashViewModel.kt`).
73+
3. Read the relevant lines (`lineNumber` from the frame +/- 30 lines of
74+
context).
75+
76+
Only map frames where `inProject` is `true`.
77+
78+
## Step 4 — Build the evidence timeline
79+
80+
### 4a. Version check
81+
82+
Read `.well-known/release-manifest.json` to get the current production and
83+
internal release versions:
84+
85+
```json
86+
{
87+
"tracks": {
88+
"production": { "versionCode": 3508, "versionName": "2026.5.2" },
89+
"internal": { "versionCode": 3508, "versionName": "2026.5.2" }
90+
}
91+
}
92+
```
93+
94+
Compare the event's `app.version` against `tracks.production.versionName` to
95+
determine if the crash still affects the latest release.
96+
97+
### 4b. Assemble evidence
98+
99+
Collect:
100+
101+
- The in-project stack frames mapped to source (file:line + code snippet)
102+
- The exception class and message
103+
- Relevant log lines (grep the `app_log` string for keywords from the exception)
104+
- The last 10-20 breadcrumbs before the crash
105+
- `app.version`, `device.manufacturer`, `device.model`, `os.version`
106+
107+
## Step 5 — Investigate root cause
108+
109+
Using the evidence from Step 4:
110+
111+
1. Read the source files identified in the stack trace.
112+
2. Follow the call chain — read callers and callees within 2 hops.
113+
3. Check for known patterns: null-safety violations, lifecycle issues,
114+
threading bugs, uncaught coroutine exceptions, missing error handling.
115+
4. Form a hypothesis and verify it against the logs and breadcrumbs.
116+
117+
## Step 6 — Propose a fix direction
118+
119+
Write a concrete fix direction (NOT a full implementation):
120+
121+
- Which file(s) to change and roughly where (`.kt:NN`)
122+
- What the fix involves (e.g. "add null check before accessing X",
123+
"move coroutine launch to lifecycleScope", "catch Y in Z")
124+
- Why this addresses the root cause
125+
- Any risks or side effects
126+
127+
## Step 7 — Route through domain experts
128+
129+
Based on the evidence, tag relevant experts by adding their labels to the brief.
130+
An issue may match multiple experts.
131+
132+
| Expert | Trigger |
133+
|--------|---------|
134+
| `compose` | Touches files with `@Composable`, `Modifier`, `remember`, `LaunchedEffect`, or under `ui/`, `features/*/ui/` |
135+
| `kotlin-coroutines` | Stack contains `CoroutineScope`, `Dispatchers`, `suspend`, `launch`, `async`, `withContext`, `Job`, `SupervisorJob` |
136+
| `android-tdd` | Proposed fix direction adds or modifies test files |
137+
| `kotlin-flows` | Stack or fix involves `Flow`, `StateFlow`, `SharedFlow`, `collect`, `stateIn` |
138+
139+
## Step 8 — Write the review brief
140+
141+
Use the template in `.claude/skills/triage/references/brief-template.md`.
142+
143+
Save the brief to `.claude/plans/triage-<error_id>.md`.
144+
145+
Keep the brief under 300 words (excluding code snippets and the evidence
146+
appendix).
147+
148+
## Step 9 — Next steps
149+
150+
If the fix direction is clear and well-scoped, offer to draft an implementation
151+
plan using `superpowers:writing-plans`.
152+
153+
If the root cause is ambiguous, suggest specific debugging steps (add logging,
154+
reproduce locally, check related Bugsnag issues).
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Triage Brief: {{title}}
2+
3+
| Field | Value |
4+
|-------|-------|
5+
| **Bugsnag** | [{{error_id}}]({{error_url}}) |
6+
| **Severity** | {{severity}} |
7+
| **Events / Users** | {{events}} / {{users}} |
8+
| **First seen** | {{first_seen}} |
9+
| **Release** | {{release}} |
10+
| **Production versionName** | {{production_version}} |
11+
| **Experts** | {{experts}} |
12+
13+
## Root Cause
14+
15+
{{1-3 sentences explaining the root cause, referencing specific `.kt:NN` locations}}
16+
17+
## Evidence
18+
19+
- **Exception**: `{{errorClass}}`: {{message}}
20+
- **Key stack frame**: `{{file.kt:NN}}``{{method}}`
21+
- **Log excerpt**: `{{relevant log line(s)}}`
22+
- **Breadcrumb trail**: {{last N breadcrumbs summarized}}
23+
- **Device**: {{manufacturer}} {{model}}, Android {{os_version}}
24+
25+
## Fix Direction
26+
27+
{{Concrete description of what to change and where (`.kt:NN`), why it fixes the
28+
issue, and any risks. NOT a full implementation — just the direction.}}
29+
30+
## Appendix: Stack Trace (in-project frames)
31+
32+
```
33+
{{mapped in-project frames with file:line and method}}
34+
```
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
# Bugsnag Event Shape — Android
2+
3+
A single event fetched from `GET /events/{event_id}` (with project auth).
4+
5+
## Four Evidence Sources
6+
7+
### 1. App Logs — `metaData["App Logs"]["app_log"]`
8+
9+
A single string containing the last ~64 KB of log output captured at crash time.
10+
Attached by `FlipcashBugsnagErrorCallback` in
11+
`apps/flipcash/app/src/main/kotlin/com/flipcash/app/internal/debug/FlipcashBugsnagErrorCallback.kt`.
12+
13+
Unlike iOS's structured `metaData.app_logs.recent_logs` array, this is
14+
unstructured text. Grep it for keywords from the exception to find relevant
15+
context.
16+
17+
### 2. Stack Trace — `exceptions[0].stacktrace`
18+
19+
Array of frame objects:
20+
21+
```json
22+
{
23+
"file": "com/flipcash/features/cash/CashViewModel.kt",
24+
"method": "com.flipcash.features.cash.CashViewModel.loadBalance",
25+
"lineNumber": 42,
26+
"inProject": true,
27+
"columnNumber": null
28+
}
29+
```
30+
31+
- `file` — path-like representation of the Kotlin/Java source file using
32+
package-qualified slashes (e.g. `com/flipcash/features/cash/CashViewModel.kt`)
33+
- `method` — fully qualified method name with package
34+
- `lineNumber` — source line (may be approximate after R8/ProGuard)
35+
- `inProject``true` for app code, `false` for framework / library code
36+
37+
**Path mapping**: Android frames use Java package paths. To find the source
38+
file, either:
39+
- Convert to a glob: `**/CashViewModel.kt` and search the repo
40+
- Or convert dots to slashes and search: `com/flipcash/features/cash/CashViewModel.kt`
41+
42+
### 3. Breadcrumbs — `breadcrumbs[]`
43+
44+
Array of timestamped events (same structure as iOS):
45+
46+
```json
47+
{
48+
"timestamp": "2026-05-10T14:23:01.000Z",
49+
"name": "Navigate to CashScreen",
50+
"type": "navigation",
51+
"metaData": { "route": "/cash" }
52+
}
53+
```
54+
55+
Types correspond to `BreadcrumbType` values: `ERROR`, `LOG`, `NAVIGATION`,
56+
`REQUEST`, `PROCESS`, `STATE`, `USER` (see `BugsnagBreadcrumbSink`).
57+
58+
### 4. Exception Info — `exceptions[0]`
59+
60+
```json
61+
{
62+
"errorClass": "java.lang.NullPointerException",
63+
"message": "Attempt to invoke virtual method 'void ...' on a null object reference",
64+
"type": "android"
65+
}
66+
```
67+
68+
Replaces iOS's `nserror` concept. The `errorClass` is the Java/Kotlin exception
69+
class name; `message` is the detail string.
70+
71+
For Kotlin-specific exceptions, look for:
72+
- `kotlin.KotlinNullPointerException`
73+
- `kotlinx.coroutines.JobCancellationException`
74+
- `java.util.concurrent.CancellationException`
75+
- `IllegalStateException` (often lifecycle-related)
76+
77+
## Secondary Context
78+
79+
| Path | Notes |
80+
|------|-------|
81+
| `app.version` | versionName (e.g. `2026.5.3`) |
82+
| `app.versionCode` | Integer version code |
83+
| `app.releaseStage` | `production` / `development` |
84+
| `device.manufacturer` | e.g. `Samsung`, `Google` |
85+
| `device.model` | e.g. `Pixel 8`, `SM-S918B` |
86+
| `device.osVersion` | Android version string (e.g. `14`) |
87+
| `device.totalMemory` | Total RAM in bytes |
88+
| `device.freeMemory` | Free RAM at crash time |
89+
| `user.id` | Anonymized user identifier |
90+
| `session` | Session start, events handled/unhandled |
91+
| `featureFlags[]` | Active feature flags at crash time |
92+
93+
## Filtering Noise
94+
95+
The app's error callback (`FlipcashBugsnagErrorCallback`) already filters:
96+
- gRPC status codes in `ErrorUtils.ignoredGrpcStatusCodes` (transport, validation)
97+
- Handled gRPC `INTERNAL` errors
98+
99+
So events that reach Bugsnag are either unhandled crashes or explicitly notified
100+
errors that passed the filter.

0 commit comments

Comments
 (0)