cmd / sentry
sentry is a Go CLI that inspects production Sentry
issues from the terminal, so a backtrace can go straight to an agent
without a trip through the web UI.
Run it with an issue's short or numeric ID:
go run ./cmd/sentry list # unresolved production issues
go run ./cmd/sentry show <ID> # latest stack trace for an issue
go run ./cmd/sentry job <ID> # queue job and company context
It reads SENTRY_AUTH_TOKEN from the environment (or .env). The
token needs event:read scope.
Two clients, opposite directions
The go/sentry article covers the runtime errs package:
it uses DSN auth to send events to Sentry. This CLI reads from
Sentry's web API using bearer-token auth against a different endpoint.
The read client lives in sentry/client.go and is shared with
cmd/deploy, which uses it to tag releases. The same
sentry.Client exposes release methods (project:releases scope) and
issue-inspection methods (event:read scope). It follows the same
HTTP client shape as go/render:
functional options, a single request method, the shared
backoff helper with 429 and 500 marked transient.
list
list queries is:unresolved environment:production and prints a
tabwriter table of short ID, count, level, and title. It does not
follow pagination; the first page covers the human-driven use case.
show
show prints an issue's latest event: metadata, tags, and the stack
trace of each exception in the chain.
Sentry's events endpoints take a numeric ID, but the ID a human copies
from the UI is a short one like EDS-4S0. So show resolves short IDs
up front:
func resolveIssueID(ctx context.Context, c *sentry.Client, id string) (string, error) {
if isShortID(id) {
numeric, err := c.ResolveShortID(ctx, sentryOrg, id)
if err != nil {
return "", fmt.Errorf("resolve short id %q: %w", id, err)
}
return numeric, nil
}
return id, nil
}
func isShortID(id string) bool {
return id != "" && id[0] >= 'A' && id[0] <= 'Z'
}
Sentry returns frames oldest-first (top of stack to the point of
failure). show reverses them for a conventional newest-first
traceback, the mirror of the ordering fix the errs client applies on
the way out:
frames := exc.Stacktrace.Frames
for i := len(frames) - 1; i >= 0; i-- {
f := frames[i]
fmt.Printf(" %s:%d in %s\n", f.Filename, f.LineNo, f.Function)
}
job
job is the piece with no cmd/deploy equivalent. Most production
errors come from the job queue, and every job event
carries a job_id tag. job reads that tag off the latest event, then
joins it against Postgres to print the queue job and, through its
co_id, the company and domain:
jobID, err := strconv.ParseInt(ev.Tag("job_id"), 10, 64)
if err != nil {
return fmt.Errorf("parse job_id %q: %w", ev.Tag("job_id"), err)
}
job, err := fetchJobByID(ctx, db, jobID)
That correlation turns "an error happened" into "this job for this
customer, with these arguments, at this time." The DB queries live in
queries/*.sql, embedded with go:embed.
Design
The Sentry coordinates (org, project) are hardcoded. eds is the
only project this CLI inspects, so a flag would add ceremony for a
value that never changes.
The event and issue structs model only the fields the CLI prints. Sentry's full event schema is large; a partial struct decodes what it needs and ignores the rest.