Investigate a Sentry issue - retrieve the issue, latest event, stack trace, tags, and breadcrumbs from kangentic.sentry.io and diagnose it. Use when a task or the user says to investigate/look at/diagnose a Sentry issue or link, or to check what errors are arriving.
Scanned 9/22/2026
Install to Claude Code
npx -y skills add Kangentic/kangentic --skill sentry --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sentry?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/kangentic-sentry)More formats (shields.io, HTML) on the badges page.
---
description: Investigate a Sentry issue - retrieve the issue, latest event, stack trace, tags, and breadcrumbs from kangentic.sentry.io and diagnose it. Use when a task or the user says to investigate/look at/diagnose a Sentry issue or link, or to check what errors are arriving.
---
# Sentry
Retrieve and diagnose issues from the `kangentic` Sentry org (`kangentic.sentry.io`). Two
projects live there: `desktop` (this Electron app, numeric id `4511996066660352`) and `mobile`
(the React Native app, numeric id `4511808149651456`). Desktop error reporting is wired in
`src/main/analytics/error-reporting.ts`; docs/analytics.md ("Error Reporting") describes what
gets captured and how.
## Auth (never print the token)
Requests need a bearer token. Resolution order (Kangentic-scoped on purpose, so another
repo's generic `SENTRY_AUTH_TOKEN` is never picked up by mistake):
1. `KANGENTIC_SENTRY_TOKEN` environment variable, if set.
2. On Windows, the User-level registry value for the same name (covers a process tree started
before the variable was set): `[Environment]::GetEnvironmentVariable('KANGENTIC_SENTRY_TOKEN','User')`.
3. The `token = ...` line in `~/.sentryclirc` (usually the CI upload token; reads 403 on it).
Read the token into a shell variable and pass it as a header in the SAME command; never echo
it, never write it to a file, never include it in a reply.
A persisted token doubles as a build switch: `scripts/build.js` and `vite.config.mts` activate
release sourcemap upload whenever `KANGENTIC_SENTRY_TOKEN` (or `SENTRY_AUTH_TOKEN`) is present
at build time. A Windows User-level value survives into every later `npm run build`, so store
the token that way only if local production builds attempting an upload is acceptable;
otherwise set it per-session. A local build that uploads writes real artifact bundles to the
`desktop` project under `Kangentic@<version>`, which is harmless (Sentry matches by debug id, so
bundles from a build nobody shipped simply go unused) but is not nothing.
Release builds run only on the CI matrix, so what actually decides whether a RELEASE gets symbols
is the `KANGENTIC_SENTRY_TOKEN` repository secret, not any local value. The `preflight-symbols`
job in `.github/workflows/release.yml` fails the release when that secret is missing.
Scopes: reading issues/events needs `event:read` + `project:read` + `org:read` (a User Auth
Token from Settings > Account > API > Auth Tokens; assigning and resolving issues additionally
need `event:write`). A `403` from every endpoint means the stored token is a CI-scoped one
(`org:ci` - it can only upload sourcemaps): stop and ask the user to mint a read-scoped token
rather than retrying. A `403` on the assign PUT alone means the token is read-only: report the
issues you could not mark and carry on, never let it block the triage itself.
## Retrieval
Parse the issue id from a pasted URL: `https://kangentic.sentry.io/issues/<ISSUE_ID>/?...`
(the `project=` query param is the numeric project id, useful for list queries).
PowerShell pattern (one call per request; substitute the endpoint):
```powershell
$token = $env:KANGENTIC_SENTRY_TOKEN; if (-not $token) { $token = [Environment]::GetEnvironmentVariable('KANGENTIC_SENTRY_TOKEN','User') }; if (-not $token) { $token = ((Get-Content "$env:USERPROFILE\.sentryclirc") | Where-Object { $_ -match '^token\s*=' }) -replace '^token\s*=\s*','' }; Invoke-RestMethod -Uri 'https://sentry.io/api/0/organizations/kangentic/issues/<ISSUE_ID>/' -Headers @{ Authorization = "Bearer $token" } | ConvertTo-Json -Depth 8
```
macOS/Linux (Bash, one command): `curl -s -H "Authorization: Bearer $KANGENTIC_SENTRY_TOKEN" <url>`.
The endpoints that matter:
| What | Endpoint |
|---|---|
| Issue summary (title, culprit, count, userCount, firstSeen/lastSeen, level, substatus) | `GET /api/0/organizations/kangentic/issues/<ISSUE_ID>/` |
| Latest event (stack trace, tags, breadcrumbs, contexts, release) | `GET /api/0/organizations/kangentic/issues/<ISSUE_ID>/events/latest/` |
| All events for the issue | `GET /api/0/organizations/kangentic/issues/<ISSUE_ID>/events/` |
| Search issues (e.g. new unresolved desktop issues) | `GET /api/0/organizations/kangentic/issues/?project=4511996066660352&query=is:unresolved&statsPeriod=14d` |
| Assign an issue (the triage marker, see below) | `PUT /api/0/organizations/kangentic/issues/<ISSUE_ID>/` body `{"assignedTo":"user:<USER_ID>"}` |
| Resolve an issue against a release (see Resolution markers) | `PUT /api/0/organizations/kangentic/issues/<ISSUE_ID>/` body `{"status":"resolved","statusDetails":{...}}` |
| Releases in the project, newest first (which versions Sentry knows) | `GET /api/0/organizations/kangentic/releases/?project=4511996066660352` |
| Org members (read `user.id` for the actor above) | `GET /api/0/organizations/kangentic/members/` |
The latest-event payload is large; extract what you need rather than dumping it: `entries`
with `type: "exception"` carries the stack frames, `type: "breadcrumbs"` the trail, `tags`
carries `source`/`reason` (stamped by `reportHandledError` for handled forwards), release,
environment, and the anonymous install id under `user.id` (non-reversible; `userCount` on the
issue = affected installs).
## Diagnosis
- **Mechanism first.** `mechanism` on the exception says how it was caught: `onunhandledrejection`
/ `onerror` (renderer globals), `generic` via `captureException` (a boundary or
`reportHandledError` - check the `source` tag: `updater`, `pty_spawn`, `spawn`, `global_db_read`,
`utility_process`). A `utility_process` event carries a `utility_process` context block under
`contexts` with the worker's stderr tail; read that before the stack, since the stack is only the
restart policy's report site and the tail is what the worker printed before it died.
- **Symbolication caveat:** packaged-release events resolve to real file/line only once a
release build uploaded sourcemaps (`KANGENTIC_SENTRY_TOKEN` set during `npm run build`;
`SENTRY_AUTH_TOKEN` is accepted as the fallback). A dev
event's renderer frames are unminified module URLs (readable); a packaged event without
uploaded maps shows minified positions - lean on message, mechanism, tags, and breadcrumbs.
- **Environment tag** separates `development` (forced-on dev/preview runs) from `production`
(packaged installs). Do not chase dev-only test events (`Kangentic telemetry verification:` is
the preview rig's own test error).
- **Exactly 50 frames means the stack is TRUNCATED, not complete.** The SDK caps a parsed stack
at 50 (`STACKTRACE_FRAME_LIMIT` in `@sentry/core`, and `Error.stackTraceLimit = 50` set by
`@sentry/browser`'s globalHandlers integration). It reads a V8 stack innermost-first and stops
there, so the frames it discards are the OUTER ones: the app code that called into the library,
and the timer or handler the whole thing ran under. Count the frames before concluding anything.
At exactly 50, `in_app: false` on every frame does NOT mean the app is uninvolved, and the
outermost frame is "the deepest point still visible", never "where it started". Once the
release carrying `tagTruncatedStack` (`src/main/analytics/error-reporting.ts`) ships, a capped
event carries a `stack_truncated: 'true'` tag; every event from before it must be counted by
hand, and so must any event with no such tag, since its absence is ambiguous until that
release is the only one reporting. DESKTOP-19 cost a whole investigation round to this: six
events, fifty monaco frames each, no in-app frame, and the app frame that armed the call
truncated away.
- **Read the `context` lines rather than reasoning from function names.** When sourcemaps are
uploaded every frame carries `context` (the source line plus surrounding lines). That is
authoritative and beats reading `node_modules` locally. Print it for the load-bearing frames:
`$event.entries | Where-Object type -eq 'exception'` then each frame's `.context`.
- **Resolve library frames against the version the RELEASE shipped**, read from `package.json` at
that git tag (`git show v0.41.0:package.json`), not from the current tree. A dependency bump
between the first-seen release and today silently invalidates every line-number mapping and
every prior "could not reproduce" measurement. DESKTOP-19 spans a monaco 0.55.1 -> 0.56.0 bump,
which both rules the bump out as the cause and means the 0.55.1 measurement behind the earlier
fix no longer describes the shipping code.
- **Same-timestamp event pairs are not always double reports.** Compare the pair's stacks before
dividing the event count: DESKTOP-19's pairs have different outermost frames, so each incident
threw twice rather than being reported twice.
- **The `mechanism` tag is evidence about the SCHEDULER, and it is checkable.**
`auto.browser.browserapierrors.setTimeout` means the chain ran inside a real `setTimeout`
callback, so enumerate the candidate timers in the implicated subsystem and rule them out one
by one. On DESKTOP-19 the obvious suspect (Monaco's background tokenizer) was eliminated from
source - it schedules with `runWhenGlobalIdle` and yields with `setTimeout0`, which uses
`postMessage` in a renderer - which left exactly one app-owned timer.
- **A default-off setting can be the missing precondition.** When an issue hits very few installs,
check whether the code path needs a non-default setting before concluding it is unreproducible.
DESKTOP-19 needs "Collapse Unchanged Regions" on, which defaults to off.
- **Cross-reference locally:** the same failure usually has a local trail - `.kangentic/logs/`
(crash JSONs, main console), `kangentic_tail_logs`, and the Aptabase `app_error` /
`spawn_failed` counts are the volume view of the same signal.
## Native minidumps (`platform: native`, mechanism `minidump`)
A native crash's frames arrive as raw addresses with `function: null` for any module Sentry has
no debug file for (node-pty's `conpty.node` / `pty.node`, `better_sqlite3.node`). They can still
be resolved offline on a Windows machine, because node-pty ships the matching PDB in its npm
tarball (`node_modules/node-pty/prebuilds/win32-x64/conpty.pdb`):
1. Read the `debugmeta` entry: for the module, take `image_addr` (the load base) and `debug_id`.
2. Confirm the shipped PDB is the same build: `dumpbin /HEADERS <path to conpty.node>` prints the
RSDS record (`{GUID}, age, pdb path`); it must equal `debug_id` (`<guid>-<age>`).
3. RVA = `instructionAddr - image_addr` for every frame in that module, `trust: scan` ones
included (scanned frames are stale, but they name what ran on this stack recently).
4. Resolve the RVAs with dbghelp from PowerShell, no debugger install needed: P/Invoke
`SymSetOptions` (undname, deferred loads, load lines), `SymInitializeW`,
`SymLoadModuleExW(hProcess, 0, <path to conpty.node>, null, 0x180000000, <size of image>, 0, 0)`
(the PDB is found next to the image), then `SymFromAddrW` and `SymGetLineFromAddrW64` at
`0x180000000 + RVA`. Function plus source line come back; this is how DESKTOP-C resolved to
`Napi::Error::ThrowAsJavaScriptException` inside `ThreadSafeFunction::CallJS`'s catch block.
5. Read the frames as a C++ story: `_CxxThrowException` is the throw site,
`__FrameHandler4::CxxCallCatchBlock` above it means the throw happened inside a catch block,
and `RtlDispatchException` / `RtlUnwindEx` further out mean an exception was already being
handled when this one was raised.
The Windows release build uploads those PDBs as Sentry debug files when the token is present
(`scripts/build.js`), so a future event should symbolicate without this. `Kangentic.exe` frames
carry names only because Electron publishes its symbols.
Reading a native event, in order of what trips people up:
- **Check for a `native_crash` context first.** `beforeSend`
(`src/main/analytics/native-crash-event.ts`) writes one from the dump itself: `crash_time`,
`crashed_version`, `uploaded_by_version`, `main_module`, `module_count`, `found_at_startup`, and
whether the release or the app context was corrected. Absence means one of two things: the event
predates that filter, or its dump could not be parsed and was therefore kept untouched. Either
way the two traps below still apply to it in full.
- **On an Electron OOM (`exit.reason: oom`, `mechanism: minidump`), read
`contexts.chromium_stability_report.system_memory_state` before anything else.** It carries
`system_commit_limit` and `system_commit_remaining` - the Windows commit charge, not physical
RAM - alongside the crashing process's own `process_states[0].memory_state.windows_memory`
(`process_private_usage`, `process_peak_pagefile_usage`, `process_allocation_attempt`). This one
field answers "did the process grow, or did the host run out" without touching a single stack
frame: DESKTOP-16 was a renderer holding 179 MB (smaller than a healthy long session) that died
because the MACHINE had 2.15 MB of commit left out of an 89.8 GB limit, with 4.66 GB of physical
RAM still free. Chromium's own OOM frames (`PartitionsOutOfMemoryUsingLessThan16M`,
`PartitionOutOfMemoryCommitFailure`) name which allocation-size bucket failed and that it was a
commit refusal, but they say nothing about whose growth caused it - `system_commit_remaining`
is the only field that separates "renderer leak" from "host exhausted" and it takes one read to
check. Also see `contexts.host_memory` if the event postdates DESKTOP-16's fix
(`src/main/diagnostics/host-memory.ts`), which carries main's own periodic sample of the same
Windows commit figures via a different, verified route (`process.getSystemMemoryInfo()`).
- **On an older event, the release tag is the UPLOADING build's, not the crashed one's.**
Crashpad writes the dump and the next launch uploads it; if the user upgraded in between, the
tag is a build that never crashed. `contexts.crashpad._version` is the build that did.
DESKTOP-M cost a triage sweep a wrong conclusion this way.
- **A shifted stack re-groups into a new issue, so "no new event on this issue" does not mean
the crash class is gone.** Native grouping keys off stack frames, and inlining, a different
thread interleaving, or an ASLR-shifted offset can move the same underlying bug into a fresh
shortId. Verifying a fix held on a later release needs a `release:Kangentic@X` query, not
`firstRelease:`, which misses a group born on an older release that still carries events on X.
Drop the `is:unresolved` and the `statsPeriod` that this skill's query examples carry. Both hide
events the release genuinely produced, which breaks the count. Sum every returned group and
check the total against that release's own event count from the releases endpoint. A match
means every event the release produced is accounted for by a named group; read each group's
title to confirm none is the crash class in question. A mismatch means the search was
incomplete, not that a recurrence is hiding. Widen it and count again. Task #669
(DESKTOP-Y/DESKTOP-Z) is where this mattered: 0.41.0's six new groups summed to its entire
16-event volume, and none was a teardown crash.
- **Breadcrumbs on a startup-found dump are not the crashed session's.** On an older event they
are the uploading launch's, wholly or partly; on a corrected one they are removed rather than
left to mislead. Breadcrumbs on an event tagged `exit.reason` are trustworthy: that tag marks
the two SDK paths that report a crash the running session watched happen.
- **A crash in a process Kangentic merely spawned no longer arrives at all.** On macOS, mach
exception ports are inherited across exec, so an agent shelling out to ffmpeg, a headless
browser, or a dotnet tool used to file its crashes as ours. Three sources have been seen:
DESKTOP-K (Homebrew ffmpeg's `ffprobe`), DESKTOP-N (a Puppeteer `chrome-headless-shell`), and
DESKTOP-Q (`/usr/local/share/dotnet/dotnet`, ten events). One filter covers all three, since it
keys off whether the dump loaded a Kangentic image rather than off any binary's name. Task #604
tracks DESKTOP-Q, though no commit names it. Those are dropped before upload now and counted as
Aptabase `foreign_minidump_dropped` instead. If a native issue looks like someone else's binary,
check that counter rather than expecting a Sentry issue.
- **Scope persists with a 500 ms write throttle**, so on any event the last half-second of
breadcrumbs before the crash is missing. An entire quit sequence fits in that gap.
## Typical requests
**"Any new issues?" (triage scan).** Query each project (or the one named) for what needs
eyes, newest first:
```
GET /api/0/organizations/kangentic/issues/?project=4511996066660352&query=is:unresolved is:for_review&statsPeriod=14d&sort=date
```
Run it for both project ids unless the user scoped to one. Report a compact per-issue line:
shortId, title, count, userCount (affected installs), firstSeen, environment, and the link
(`https://kangentic.sentry.io/issues/<id>/`). Two filters keep the report honest:
- Treat `environment: development` events as dev/preview noise (the
`Kangentic telemetry verification:` issues are the rig's own test errors) - list them
separately or not at all, never alongside production issues without saying so.
- "New" means new to the user: an **unassigned** issue whose shortId appears in no board task.
Assignment is the triage marker (see below), so start the sweep by reading `assignedTo` on
each issue and treat an assigned one as already looked at. Still confirm with
`kangentic_search_tasks` for the shortId, because assignment can be stale and a task can
exist without one, but an assigned issue is never reported as new.
**"Investigate this issue / create a follow-up task."** Retrieve the issue and latest event,
diagnose (below), then - when asked for a task - create ONE task via the kangentic MCP tools,
routed by project: a DESKTOP-* issue goes on the `kangentic` board; a MOBILE-* issue (or a
REACT-NATIVE-* one - issues created before the 2026-08 slug rename keep their old prefix) on
`kangentic-mobile`. First search for an existing task carrying the shortId so a re-report never
duplicates. Title: `Fix DESKTOP-N: <issue title, trimmed>`. Description: the Sentry link,
shortId, level, event/affected-install counts, environment + release, the diagnosis, and the
few stack frames or tags that carry it. Default to To Do; the user decides when it spawns.
Three fields belong in that description because leaving them out is what sends the next
investigation down a wrong path:
- **The frame count, and whether the stack is truncated** (see Diagnosis). "50 frames, truncated,
no app frame survived" and "47 frames, genuinely all third-party" call for completely different
work, and the event itself does not say which.
- **The release-to-dependency-version mapping** for whatever library the stack lands in, read
from `package.json` at each affected release's tag. It is what tells the next reader whether a
version bump is a suspect or already ruled out.
- **Whether the event count is distinct incidents or paired reports**, and any non-default setting
the path requires.
Never paste a stack frame's raw `file:///` URL or `absPath` into the task: those carry a
contributor's home directory, and the board is mirrored to a public repo. Cite frames by module
path and line (`viewModelImpl.js:145`).
**Then assign every issue the task covers.** Creating a board task and leaving the Sentry issue
unassigned means the next sweep re-derives the whole cross-reference from scratch, which is what
assignment exists to prevent here. Assignment is a triage marker, not a claim of ownership: it
says a human has looked at this and it has a home. Rules:
- Assign after the task is created, never before, so a failed create cannot leave a false marker.
- One task can cover several issues (a cluster, or several issues that resolve in one file).
Assign all of them, not just the one that named the task.
- Assign issues covered by an EXISTING task too when a sweep turns one up unassigned. The signal
is only useful if it is complete.
- Never assign an issue with no board task, and never assign dev/preview rig noise. An unassigned
issue must keep meaning "nobody has dealt with this".
- Resolve nothing here. Assignment leaves the issue in the unresolved stream where a recurrence
is still visible, which is the whole point: a fix that does not hold shows up as new events on
an assigned issue rather than disappearing. A native crash is the exception, because a shifted
stack re-groups into a fresh shortId and the assigned issue stays silent (see Native minidumps
above). Assignment holds until the fix actually ships, so resolution is a release-time act, not
a triage one. See Resolution markers below.
## Resolution markers
Resolution is a release-time act. The marker names the release that CARRIES THE FIX, never the one
the issue was last seen on. Which release that is depends on where you are standing. From triage,
before the fix has shipped, the newest release is always the wrong answer, because it predates the
fix. From `/release` Step 8, after the build is published, the version just shipped is the carrier
and is the right answer. Step 8 is where this normally happens; do it by hand only to correct a
marker that is already wrong.
Sentry keeps two resolution types, and they reopen an issue on different events:
- `in_release` against X: an event on X itself reopens the issue, and only releases older than X
stay suppressed. Naming the current release therefore reopens the issue on exactly the builds
that legitimately lack the fix.
- `in_next_release` against X: events on X and older stay suppressed, and anything newer reopens.
That is what "fixed in the release after X" means.
Which body to send:
| Situation | Body of the resolve PUT |
|---|---|
| The release carrying the fix exists in Sentry | `{"status":"resolved","statusDetails":{"inRelease":"Kangentic@X.Y.Z"}}` |
| It does not exist yet (the normal case before that release builds) | `{"status":"resolved","statusDetails":{"inNextRelease":true}}` |
Use the `Kangentic@X.Y.Z` form, not the `vX.Y.Z` git tag. Each release object is created by the
bundler plugin during its own CI build (`scripts/build.js`, `vite.config.mts`), so the version a
pending fix will ship in does not exist yet, and `inRelease` on it fails with a 400 and
"Unable to find a release with the given version."
Four things bite:
1. **A resolve PUT against an already-resolved issue is silently a no-op.** It returns 200 with the
full group payload and changes nothing. To correct a marker, PUT `{"status":"unresolved"}`
first, then PUT the resolution. Both writes, in that order, every time.
2. **The read-back cannot tell the two types apart.** `statusDetails` renders
`inRelease: Kangentic@0.39.0` both for a real `in_release` against 0.39.0 and for an
`in_next_release` recorded against it, so it cannot confirm a write landed. Verify with the
issue payload's own `activity` array: the newest `set_resolved_in_release` entry carries a
populated `version` for an in-release resolution, and an empty `version` plus
`current_release_version` for an in-next-release one.
3. **The GitHub integration resolves issues without being asked.** Merging a PR whose body names a
shortId writes a `set_resolved_in_pull_request` entry, and commit-to-release association can
follow it with a `set_resolved_in_release` naming whichever release was current then. DESKTOP-J
carried a wrong marker from that path, not from a hand action. So expect an issue to arrive at
the release step already resolved, and correct it rather than assuming a human chose it.
4. **A 403 on the resolve PUT means the token is read-only or CI-scoped.** Report which issues went
unmarked and carry on. Never retry in a loop, and never let it block a release.
## Boundaries
- Diagnose and report; fix only when the task asks for a fix.
- Create a follow-up board task only when asked ("create a follow up task" style requests):
include the Sentry link, shortId, affected-install count, and your diagnosis in the
description.
- Assigning an issue you just filed a task for is sanctioned and expected, no separate ask
needed. It is the one write this skill makes on its own.
- Do not resolve/archive issues in Sentry unless explicitly asked (needs `event:write`). Resolving
belongs to `/release` Step 8, which marks the issues a release fixes once that release exists;
from here, resolve only to correct a marker that names the wrong release. Either way follow
Resolution markers above.
- Never paste the token or a full raw event dump into a task, commit, or reply; quote the
frames and fields that carry the diagnosis.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!