cmd / sentry

sentry is a Go CLI that inspects production Sentry issues from the terminal, so I can paste a backtrace into an agent.

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

The go/sentry article covers the runtime errs package, which uses DSN auth to send events to Sentry. This CLI reads from Sentry's web API with bearer-token auth.

The read client lives in sentry/client.go. cmd/deploy shares it to tag releases. The same sentry.Client exposes release methods (project:releases scope) and issue methods (event:read scope). It follows the same HTTP client shape as go/render: functional options, one request method, and 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.

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 UI shows a short ID like APP-4S0. show resolves a short ID first:

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. show reverses them for a newest-first traceback:

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

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 queries Postgres for 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)

The database queries live in queries/*.sql, embedded with go:embed.

Design

The Sentry org and project are hardcoded. app is the only project this CLI inspects.

The event and issue structs model only the fields the CLI prints.

← All articles